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 Hobbyproduces 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 --confirmSame 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-previewson a schedule; the default listing lies by omission. - On Hobby, either set per-service PR previews to Manual (opt in with the
render-previewlabel) 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.