Render Tutorials
Postgres on Render: a deep dive

Storage and autoscaling

⏱ 7 min

Storage on Render Postgres is one-way. You can grow it - manually or via autoscaling - but you can never shrink it. That asymmetry decides almost every operational habit in this step.

This is also one of the few areas where Render quietly does work for you in the background, so it pays to know exactly when and how.

How autoscaling works

flowchart LR
  use["Disk usage hits ~90%"]
  trigger["Render autoscale triggers"]
  grow["Grow by ~50%,<br/>rounded up to next 5 GB"]
  cooldown["12-hour cooldown<br/>before next growth"]
  max["Hard ceiling: 16 TB"]

  use --> trigger --> grow --> cooldown
  grow -.-> max

The rules:

KnobBehavior
Trigger thresholdRoughly ~90% disk usage
Growth amountRoughly ~50%, rounded up to the next 5 GB multiple
Maximum16 TB
Cooldown after a growth12 hours before another autoscale
ShrinkNever - disk increases are permanent

A worked example: a 10 GB disk that fills past ~9 GB triggers a growth to ~15 GB (10 × 1.5 = 15, already a 5 GB multiple). The next growth would happen at ~13.5 GB usage, no sooner than 12 hours after the first one.

If the rate of growth is faster than autoscaling can keep up - say your app suddenly inserts gigabytes per hour - you can hit the limit between autoscale events and degrade. We cover what that looks like below.

What “can never shrink” actually means

flowchart LR
  s1["10 GB"] -->|"fills"| s2["15 GB<br/>(autoscaled)"]
  s2 -->|"app deletes data"| s3["Still 15 GB<br/>(disk doesn't shrink)"]
  s3 -->|"export + recreate"| s4["10 GB on new instance"]

If you delete data from a Postgres table, three things happen:

  • The rows are marked dead but stay on disk until VACUUM reclaims them.
  • After VACUUM, the table-level free space is reusable for future inserts.
  • The disk-level allocation doesn’t change. Render’s view of the disk is still 15 GB.

So “shrinking” in any meaningful sense means archiving data and recreating the database on a smaller disk. That’s an export-restore migration, not an in-place operation. The same flow as changing an immutable field, covered in step 02.

The only way to get a smaller disk number is to be on a fresh instance. So the operational habit:

Archive aggressively before you autoscale, not after.

Suspension at the hard limit

Autoscaling has the 16 TB ceiling and the 12-hour cooldown. If your disk fills past the configured limit faster than autoscaling can react - or you’re at 16 TB - Render may suspend the database until you resolve the situation.

A suspended database accepts no connections. Your app sees connection failures. The fix is to:

  1. Stop the writes that are filling the disk Pause workers that produce data. Pause batch jobs. Whatever’s adding bytes, stop it.
  2. Vacuum to reclaim freed space If you’ve deleted rows, VACUUM (or VACUUM FULL for aggressive cases - note it locks the table) makes the freed space reusable.
  3. Archive cold data COPY historical rows to S3 or another store, then DELETE and VACUUM.
  4. Manually grow the disk Up to the plan’s maximum if autoscaling isn’t fast enough.

You don’t want to be doing any of this under pressure. The next section is how to avoid it.

How to monitor disk before it hurts

Three layers of monitoring, in order of how often you’ll use them:

1. Render Dashboard graphs

The Postgres instance page in the Render Dashboard has a Storage graph that plots disk usage over time. Glance at it weekly. The trend line tells you if you’re going to hit autoscale next month or in six months.

2. The metrics API / MCP

For programmatic monitoring (alerts, dashboards), use Render’s metrics endpoints. Via the MCP server:

MCP query
get_metrics(
resourceId: "<postgres-id>",
metricTypes: ["disk_usage_percent"]
)

You can hit a similar endpoint via the API directly if you’re not using MCP. Wire it into your existing alerting (Datadog, Grafana, plain cron + Slack webhook) so you get notified at, say, 70% usage - well before autoscale even thinks about firing.

3. SQL: pg_database_size and table-level breakdown

When you need to know what’s using the space:

psql
SELECT pg_size_pretty(pg_database_size(current_database())) AS db_size;

For a per-table breakdown:

psql
SELECT
schemaname,
relname,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
pg_size_pretty(pg_relation_size(relid)) AS table_size,
pg_size_pretty(pg_total_relation_size(relid) - pg_relation_size(relid)) AS index_and_toast_size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC
LIMIT 20;

The result is sorted largest-first. Tables at the top are the ones to archive or partition. Indexes and TOAST data (large field storage) are part of the total - sometimes the index is bigger than the table.

The pre-autoscale playbook

When usage starts climbing toward 70-80%, you have time to act before autoscale kicks in. Three moves, in order:

  1. Check for bloat Run the pg_stat_user_tables query from the operations step. High n_dead_tup means autovacuum is falling behind. A targeted VACUUM ANALYZE reclaims space without growing the disk.
  2. Drop unused indexes Indexes you never query are pure cost. pg_stat_user_indexes shows usage. The “indexes never scanned” query (in step 09) finds them quickly.
  3. Archive old data Move cold rows out - to S3, to a separate “archive” Postgres instance, or to a partitioned table you can detach later. Then DELETE + VACUUM.

Step 3 is the only one that frees real disk. The first two free internal space - Postgres can reuse it, but the disk allocation stays the same. That’s still useful: it pushes out the day you’d autoscale.

Setting initial size and the manual growth knob

You picked an initial size at creation (diskSizeGB in the Blueprint, or a slider in the Render Dashboard). You can manually grow it any time, on any plan:

render.yaml
databases:
- name: app-db
plan: basic-1gb
diskSizeGB: 20

Increase the number, sync, Render grows the disk in the background. There’s no downtime for storage growth - the database keeps serving traffic while the volume resizes.

A worked sizing decision

A common conversation:

“Should I provision 10 GB or 50 GB to start?”

The math, since storage is mostly variable cost based on what you allocate:

  • 10 GB: small bill day one, but you’ll likely autoscale within months. Each autoscale costs nothing extra in dollars (Render bills the actual size you have), but you can only autoscale once per 12 hours, and the rounding is conservative.
  • 50 GB: larger bill day one, but you can run for a year or more with no surprises.

The right answer for most production apps: start at the size that covers your next 3-6 months of growth. Autoscaling exists for the case where your 6-month estimate is wrong, not as the primary growth strategy.

For dev/staging databases, the minimum (1 GB) is almost always fine. They rarely accumulate enough data to autoscale.

Your production database is on a 10 GB disk. It's currently using 9.3 GB. Earlier today you ran a large `DELETE` that removed millions of rows; you confirmed `VACUUM ANALYZE` ran successfully on the affected tables. The Render Dashboard still shows 9.3 GB usage, and you're worried about autoscaling. What's happening?

What you learned

  • Storage autoscales at ~90% usage by ~50% (rounded to next 5 GB) up to 16 TB, with a 12-hour cooldown between growths
  • Disks can grow but never shrink - getting a smaller disk requires export + restore to a new instance
  • Monitor proactively: Render Dashboard graph for trends, MCP `get_metrics` for alerts, `pg_database_size` and `pg_total_relation_size` for what's eating the space
  • `VACUUM` reclaims internal space (reusable for future inserts) but doesn't shrink the disk allocation
  • Pre-autoscale moves: vacuum dead tuples, drop unused indexes, archive cold data. Only archiving frees real disk; the others delay the next autoscale