Skip to content

Two ways Render keeps billing you after you think the resource is gone

TL;DR.

previews.expireAfterDays in render.yaml only governs Pro-plan preview environments, so per-service PR previews on Hobby live until the PR closes; and a Postgres major-version upgrade leaves behind an -upgrade-clone-NNNN instance that nothing deletes. Both are invisible in the service list unless you ask for previews explicitly.

If you set previews.expireAfterDays in render.yaml and assume stale PR previews clean themselves up, check whether your workspace is even eligible for the feature that key controls. On a Hobby workspace it is dead config, and your preview instances bill until the PR is closed. A second, quieter leak: Render's Postgres major-version upgrade leaves a clone instance running indefinitely.

Both were found auditing a small Render workspace (Render CLI v2.20.0, API v1, September 2026) for forgotten resources. Neither shows up if you just look at the dashboard service list.

expireAfterDays controls preview environments, not service previews

Render has two distinct features with confusingly similar names:

Feature Scope Config Expiry
Preview environments Full copy of an environment (services + datastores) per PR Blueprint root previews.generation / previews.expireAfterDays Auto-deleted on PR close, plus optional inactivity expiry
Service previews (PR previews) One temporary instance of a single service Per-service dashboard Previews tab, or service-level previews.generation Deleted only when the PR is merged or closed. No expiry, ever.

Preview environments require a Pro workspace or higher. On Hobby you get "single-service previews" instead. So a blueprint like this:

services:
  - type: web
    name: my-frontend
    previews:
      generation: automatic
previews:
  expireAfterDays: 4   # silently inert on Hobby

produces per-service PR previews with no expiry at all. The root-level expireAfterDays is accepted, never warned about, and never fires. A long-lived open PR keeps a full-price instance alive for weeks. Billing is the base service's rate, prorated by the second — a starter web service preview is $7/month for as long as the PR stays open.

The tell that you have service previews rather than preview environments: the preview service's environmentId is your existing stage/prod environment, and serviceDetails.parentServer points at the base service. A real preview environment would have its own environment id.

Also worth knowing: a monorepo rootDir or build filter means the preview only rebuilds when files under that path change. A preview can therefore be both alive and serving a commit many commits behind the PR head, which makes it actively misleading to reviewers.

Preview resources are hidden from the default listing

render services does not show previews. You must opt in:

render services --include-previews -o json --confirm

Same on the REST API: GET /v1/services?includePreviews=true. Image previews (created via POST /v1/services/{id}/preview) are worse — Render never deletes those automatically, and there is no PR close event to trigger cleanup.

Postgres major-version upgrades leave a clone behind

After a Postgres major-version upgrade, the workspace contained a second instance named <original>-upgrade-clone-NNNN in the same environment, status available, suspended: not_suspended, same plan (basic_1gb) and same 15 GB disk as the original. Seven months later it was still running and still billing (~$19/month for the instance plus expandable storage at $0.30/GB-month).

It is easy to miss because the clone looks exactly like a real database in every listing. Distinguish the live one from the fossil by querying both, not by reading names or timestamps:

render psql <postgres-id> --confirm -o text -c \
  "select datname, pg_size_pretty(pg_database_size(datname)) sz,
          (select count(*) from pg_stat_activity a where a.datname=d.datname) conns
     from pg_database d order by pg_database_size(datname) desc limit 3;"

The live instance showed 3507 MB and 19 connections; the clone showed 126 MB and 0 connections for the same database name. Size divergence plus zero connections is unambiguous. Note that current_database() on a Render Postgres connection is the instance's default database, which is often an empty 7766 kB shell — query pg_database instead or you will conclude your production database is 7 MB.

What to do

  • Audit with --include-previews on a schedule; the default listing lies by omission.
  • On Hobby, either set per-service PR previews to Manual (opt in with the render-preview label) or accept that previews live as long as the PR does. Delete the config key that does nothing, or comment it as Pro-only so the next reader is not fooled.
  • After any Postgres major-version upgrade, go looking for -upgrade-clone- by name and delete it once you have verified zero connections and taken a dump.
  • Manually deleting a PR preview for a still-open PR is temporary: Render recreates it on the next push that matches the service's root directory or build filter.
No signals yet