Render Tutorials
Advanced render.yaml Blueprint patterns

Preview environments

⏱ 8 min

A preview environment is a temporary copy of your services and databases, spun up automatically (or on demand) when a pull request opens. Every reviewer gets a real URL with the PR’s code running on it, and the whole thing tears itself down after a few days.

Done right, it’s the most useful feature of render.yaml. Done carelessly, it costs more than your production environment.

The two top-level knobs

render.yaml
previews:
generation: automatic
expireAfterDays: 3

That’s the whole top-level surface. Two fields, big behavioral differences.

previews.generation

ValueBehavior
off (default)No previews. The Blueprint behaves as if previews: weren’t there.
manualPreviews don’t auto-create. A teammate clicks “Create preview” in the Render Dashboard for the PRs they want to spin up.
automaticEvery eligible PR gets a preview. Closing or merging the PR tears it down.
flowchart LR
  pr["Pull request opened"]
  off["off → ignored"]
  manual["manual → wait for click"]
  auto["automatic → preview spins up"]

  pr --> off
  pr --> manual
  pr --> auto

automatic is what most teams want once they’ve trusted previews for a few PRs. Start with manual while you’re tuning the Blueprint, then graduate.

previews.expireAfterDays

A positive integer auto-deletes the preview stack N days after creation, regardless of PR state. Without expiry, a long-lived PR (or a closed PR whose teardown silently failed) keeps charging your account until you notice.

A safe default for most teams: expireAfterDays: 3. That covers a typical PR review cycle and forces a re-create on stale work, which is usually what you want anyway.

Per-service overrides

Sometimes you want previews - but not for every service. A worker that posts to your real billing webhook? Skip it. A cron that emails customers on a schedule? Definitely skip it.

render.yaml
previews:
generation: automatic
services:
- type: web
name: api
runtime: node
plan: starter
buildCommand: npm ci && npm run build
startCommand: npm start
- type: worker
name: billing-webhook
runtime: node
plan: starter
buildCommand: npm ci
startCommand: node webhook.js
previews:
generation: off

The web service inherits automatic. The worker opts out at the service level. Same syntax for cron jobs and private services.

Smaller plans for previews

Production runs on standard. Previews don’t need that - they get one reviewer’s traffic, not your customers’. previewPlan lets each service downsize for previews:

render.yaml
services:
- type: web
name: api
runtime: node
plan: standard
previewPlan: starter
buildCommand: npm ci && npm run build
startCommand: npm start

The same field works on databases via previewPlan and previewDiskSizeGB:

render.yaml
databases:
- name: app-db
plan: basic-1gb
previewPlan: basic-256mb
previewDiskSizeGB: 5

The sync: false gotcha that breaks most preview deploys

This is the single most-asked question in #help-render:

“My preview deployed but the app crashes with STRIPE_API_KEY is undefined. Production works fine.”

Cause: sync: false env vars are not propagated to preview environments. Each preview spins up empty. The app boots, doesn’t find the key, and crashes.

flowchart LR
  prod["Production service<br/>STRIPE_API_KEY=sk_live_..."]
  pr["Pull request"]
  preview["Preview service<br/>STRIPE_API_KEY=undefined"]

  prod -.->|"sync: false<br/>doesn't copy"| preview
  pr --> preview
  preview --> crash["Boot failure"]

Three ways out, in increasing order of effort:

  1. Use a sandbox key for previews. Move the var out of sync: false and into a value: that points at your provider’s test key. Stripe, OpenAI, Anthropic, Twilio - all have sandbox credentials safe to commit.

  2. Pull the secret from a workspace env group. Workspace-scoped env groups are available in previews. Move the secret into a Render Dashboard-managed group, attach it to the service via fromGroup, and previews inherit it without sync: false.

  3. Inject from CI. Your PR pipeline writes the secret into the preview after creation via the Render API. More moving parts, but it’s the only option when the secret is per-PR (e.g. test data isolated to that PR).

Option 2 is the right answer for most teams. The rule: anything sensitive enough for sync: false is sensitive enough for an env group.

Autoscaling and previews

flowchart LR
  prod["Production: scaling<br/>min 2, max 10"]
  preview["Preview: pinned to min 2"]

  prod -.->|"autoscaling disabled<br/>in previews"| preview

The implication is mostly cost. A heavy-instance autoscaling service that serves a lot of traffic in production might run a single preview instance for $0/PR (with a small previewPlan) or hundreds of dollars per month at the production minimum. Set previewPlan aggressively.

A complete preview-aware Blueprint

render.yaml
previews:
generation: automatic
expireAfterDays: 3
databases:
- name: app-db
plan: basic-1gb
previewPlan: basic-256mb
previewDiskSizeGB: 5
services:
- type: web
name: api
runtime: node
plan: standard
previewPlan: starter
buildCommand: npm ci && npm run build
startCommand: npm start
envVars:
- key: DATABASE_URL
fromDatabase:
name: app-db
property: connectionString
- key: STRIPE_API_KEY
fromGroup: shared-secrets
- type: worker
name: billing-webhook
runtime: node
plan: starter
buildCommand: npm ci
startCommand: node webhook.js
previews:
generation: off
envVarGroups:
- name: shared-secrets
envVars:
- key: STRIPE_API_KEY

Walk it: previews on for the project, off for the billing worker; smaller plans for both web and DB; secrets via a workspace group instead of sync: false. That’s the preview-friendly pattern.

Your `render.yaml` has `previews.generation: automatic` and a service env var declared as `- key: STRIPE_API_KEY` with `sync: false`. A teammate opens a PR. What happens?

What you learned

  • `previews.generation`: `off` (default), `manual`, `automatic`. Set `expireAfterDays` to cap cost
  • Per-service `previews.generation: off` opts out workers, crons, or private services that shouldn't run in previews
  • `previewPlan` (and `previewDiskSizeGB` on databases) downsizes preview instances; can't cross flexible / non-flexible families
  • `sync: false` is excluded from previews - switch the var to a workspace env group with `fromGroup`, or use sandbox keys
  • Autoscaling is disabled in previews - services run at their minimum instance count