Render Tutorials
Web service or static site?

Migration paths and free-tier traps

⏱ 6 min

You picked one. Six months later, the world’s changed: the static site needs a /contact form that emails you, or the web service is paying for an instance to serve mostly static files. This step is the playbook for migrating.

The immutable rule that shapes every migration

The same rule covered in the advanced Blueprint patterns tutorial, here in its frontend-shaped form. Plan migrations with that rule in front of you - it decides the shape of every option below.

Path 1: Static site → static site + new API

The most common migration. You shipped a static SPA, and now you need a few server-side endpoints - a contact form, an auth callback, a Stripe webhook receiver.

Don’t migrate the static site to a web service. Add a separate web service alongside it.

flowchart LR
  before["Before:<br/>my-frontend (static)"]
  after1["After: my-frontend (static)"]
  after2["After: my-api (web service)"]

  before --> after1
  before --> after2

In the Blueprint:

render.yaml
services:
- type: web
runtime: static
name: my-frontend
buildCommand: npm ci && npm run build
staticPublishPath: dist
envVars:
- key: VITE_API_URL
value: https://my-api.onrender.com
- type: web
name: my-api
runtime: node
plan: starter
buildCommand: npm ci && npm run build
startCommand: npm start
healthCheckPath: /health
envVars:
- key: ALLOWED_ORIGIN
value: https://my-frontend.onrender.com

Sync the Blueprint, the new API service spins up, the frontend rebuilds with VITE_API_URL baked in, browser calls hit the new API. No downtime, no DNS changes for existing static-site users.

This is the hybrid pattern by another name - most teams arrive at it through this exact migration.

Path 2: Web service serving static files → split into static + API

Going the other way. You started with a web service that serves both your built React app and an API. It works, but you’re paying for an instance that’s mostly serving static files, and free-tier cold starts are hurting first-load times.

Split it. The frontend becomes a static site (free CDN, no cold starts, free custom domains), the API stays as a web service.

flowchart LR
  before["Before:<br/>my-app (web service)<br/>serves React + /api/*"]
  after1["After: my-frontend (static)"]
  after2["After: my-api (web service)<br/>only /api/*"]

  before --> after1
  before --> after2

The migration steps:

  1. Strip the API routes out of the frontend repo Or split into a monorepo with an apps/web and an apps/api folder. The buildCommand on the static site only builds the frontend; the API service builds and runs only the API.
  2. Add the static site to the Blueprint A new service: runtime: static, staticPublishPath: dist, the SPA fallback rewrite. Don’t touch the existing web service yet.
  3. Verify the static site at its new URL Open my-frontend.onrender.com and confirm pages load and API calls (still pointing at the old web service URL) work.
  4. Cut over DNS or update VITE_API_URL Either repoint your custom domain to the static site, or swap VITE_API_URL in the static site to point at the renamed API service.
  5. Slim down the old web service Remove the static-file-serving routes. The web service becomes API-only.

The win, in concrete numbers: the static frontend gets free bandwidth from the CDN (counted against your workspace’s monthly free amount), no cold starts, and free custom domains. The API service can drop to a smaller instance because it’s no longer serving asset bytes.

Path 3: Static site → SSR web service

Less common, but it happens. You started with a Vite SPA, and now you need full server-side rendering for SEO, social previews, or per-request auth on every page.

This is the case where you genuinely replace the static site with a web service. The migration:

  1. Rewrite (or restructure) the frontend as an SSR-capable framework Next.js with next start, Nuxt with the Node adapter, SvelteKit with the Node adapter, Remix, etc.
  2. Add the new web service to the Blueprint A different service, type: web, runtime: node, with a startCommand like npm start. Different name than the old static site.
  3. Verify the new web service at its onrender.com URL Confirm pages render, the API behaves correctly, and your auth flow works server-side.
  4. Repoint your custom domain From the static site to the new web service. The old static site becomes a *.onrender.com-only fallback you can delete after a week.
  5. Delete the static site Once the custom domain is on the new web service and you’re confident, remove the static-site service from the Blueprint.

Two things to expect:

  • Cold starts are back. Even on paid plans, an SSR web service has a process to start. Plan for it in your perceived performance budget.
  • Bandwidth costs change shape. You’re now paying for the web service’s bandwidth, not the static site’s. Usually similar; sometimes more.

Free-tier traps to remember

A condensed list of the gotchas that pop up most often in #help-render:

TrapWhere it bites
Free web services spin down after 15 min idleFirst request after sleep is a cold start; scheduled hits to keep it warm work but feel like a hack
Free web services don’t get custom domainsWant mysite.com? Either ship it as a static site (free) or pay for a non-free instance type
Static sites have no env vars at runtimeVITE_API_URL is baked at build; changing it requires a rebuild
Static sites can’t reach the private networkBrowser → API hits public URLs only. CORS is required
runtime is immutableEvery migration is a new service alongside the old, not an in-place flip
Build minutes count against the workspace, not per serviceA monorepo with 5 frontends doesn’t get 5× the minutes
Bandwidth caps are workspace-wideHeavy traffic on one service eats the budget for the whole workspace

The first two cover most of “wait, why is my free-tier site behaving weirdly” tickets. The middle three cover most of “the migration broke something” tickets. The last two are the ones that surprise teams who scaled up to many services.

Where to go next

You now have the full picture for picking and migrating between Render’s two main service shapes. Natural next stops:

  • Advanced Blueprint patterns - the cookbook of patterns for multi-service apps. Projects + environments, env groups, cross-service wiring, previews, monorepos, disks, scaling, and Docker/image runtimes.
  • Render Workflows quickstart - when “I need code that runs in response to events” turns into a fan-out, retry-aware function rather than a web request.
  • The Render docs - static sites, web services, and free-tier behavior - the source of truth for any field you don’t recognize.
Your `render.yaml` defines a service named `my-app` with `runtime: static`. You edit the Blueprint to change `runtime: static` to `runtime: node` and add a `startCommand`, expecting Render to redeploy as a web service. What happens on the next sync?

What you learned

  • Static site needs an API endpoint? Add a new web service alongside it. Don't try to migrate the static site
  • Web service serving lots of static files? Split into static + API for a free CDN and no cold starts
  • Going to SSR? It's a real replacement - new web service, repoint DNS, delete the static site once verified
  • `runtime` is immutable. Every migration is a new service alongside the old, never an in-place flip
  • Free-tier traps: services sleep, no custom domains on free web services, no runtime env vars on static sites, no private network access for static sites