all posts

Suspending idle Postgres databases

Ajay Kumar··8 min read

Take a product that gives every customer their own database. Two hundred customers, two hundred databases, and at any moment maybe fifteen are being queried. The other 185 are running, holding memory, and billing at exactly the same rate as the busy ones.

That's the case for suspending idle databases, and it's much stronger than the equivalent argument for web apps, because databases are heavier and the idle tail is longer. It's also harder to implement well, because a database that's asleep when a connection arrives cannot simply refuse it. I build PandaStack, which does this, and this post is about the mechanism and the sharp edges.

The fleet shape that makes this worth doing

  • Per-customer databases in a B2B product. Customers work during their business hours, in their timezone, and most are not doing that right now.
  • Preview and staging databases. One per branch or pull request, queried during a review and then never again until someone remembers to delete it.
  • Internal tools. A database behind an admin panel used twice a week.
  • Development databases. Busy between 10am and 6pm on weekdays, which is 24% of the week at best.
  • Long-tail archives. Data that must remain queryable but is queried rarely.

The common factor is a duty cycle in the single digits. If your database serves continuous production traffic, none of this applies and you should stop reading — leave it running, and don't add a wake-latency risk to something that never idles.

What suspend and wake actually do

Suspending a database is not the same as stopping a web app, because the data must survive and be exactly as it was. The sequence matters.

  1. Detect idleness — no client connections and no query activity for the configured period.
  2. Quiesce cleanly. Not a kill: Postgres gets to finish in-flight transactions and reach a consistent state, so waking is a clean start rather than crash recovery.
  3. Capture state and release the compute. The durable volume holding the data directory persists; the VM's memory and vCPUs go away.
  4. On the next connection attempt, hold the incoming connection, restore the machine, wait for Postgres to accept connections, then complete the client's connection through to the live database.

Step four is the whole trick. If the proxy in front refuses the connection while waking, every client sees an error and your users experience 'the database is down' rather than 'the first query was slow'. Holding the connection until the backend is ready turns an outage into latency, which is a much better failure mode — provided the client's connect timeout is longer than the wake.

Check your client's connection timeout before enabling this. Many defaults are 10 to 30 seconds. If a cold wake can exceed that, the client gives up mid-wake, retries, and you get a thundering herd of retries against a database that is in the middle of starting.

Connection pools change the calculation

Here's the detail that decides whether auto-suspend works at all for your application, and it catches everyone.

Most application frameworks open a connection pool at startup and keep those connections open indefinitely, with a keepalive so they don't go stale. From the database's perspective that looks like a permanently connected client. If your idle detection is 'no open connections', the database never idles — not because anyone is using it, but because a pool is sitting there holding ten sockets and pinging occasionally.

This is the database version of the bug where an uptime monitor keeps a web app permanently awake, and it has the same fix: measure the right thing. Idleness has to mean 'no queries' rather than 'no connections', and ideally 'no queries other than the ones a pool sends to keep itself alive'. Otherwise you ship a feature that never triggers, and the only symptom is that the bill doesn't change.

  • Configure pool idle timeouts on the application side so idle connections actually close — most pool libraries support this and most teams never set it.
  • Distinguish keepalive traffic from real queries in your idle detection.
  • For applications that themselves sleep, this resolves itself: the app sleeps, its pool disappears, the database idles shortly afterwards. Suspending apps and databases together is much cleaner than suspending either alone.

What breaks in your application

Suspension makes visible a set of assumptions applications quietly make about databases always being there.

  • The first connection after a wake is slow. Everything else is normal. If your monitoring alerts on any query over N milliseconds, you'll get an alert every wake — tune it or you'll train yourself to ignore the channel.
  • Health checks that query the database will wake it constantly. If your app's liveness probe runs a query every 30 seconds, congratulations, you have built a keepalive. Health checks should not touch the database, for this reason and several others.
  • Background jobs at odd hours wake it. A nightly cleanup task means the database wakes nightly, which is correct but should be intentional rather than a surprise.
  • Retry logic needs to exist and be sane. A connection failure during wake should be retried with backoff, not immediately and not infinitely. Most drivers do something reasonable; check rather than assume.
  • Metrics gaps. A suspended database emits no metrics. Dashboards will show gaps, and alerts on 'no data' will fire. Configure them to treat suspension as an expected state.

The billing correctness problem

A detail from the implementation side that matters if you're building this. Once compute is suspended, you must stop metering it — and getting that wrong in either direction is bad.

Over-billing means charging for a database that isn't running, which customers will find and will not be quiet about. Under-billing is silent revenue loss. Both bugs come from the same root: usage is measured against lifecycle transitions, and pause, wake, restart, migration, and crash are all transitions that must finalise the previous usage window correctly. We fixed a real over-billing bug here, and the fix was to make teardown and pause explicitly finalise the usage record rather than relying on a periodic sweep noticing later.

If you're evaluating a platform that offers this, ask exactly what stops billing when a database suspends: compute, storage, or both. Storage almost always continues, correctly — your data still occupies a durable volume. What should stop is memory and vCPU.

When to leave it running

Auto-suspend is off by default on our platform, and I think that's the right default. It's a decision about a trade you must make deliberately.

  • Production databases with real traffic. They never idle, so there is no saving and only risk.
  • Anything where the first-query latency is customer-visible. A B2B admin tool where the first click takes a few extra seconds is fine. A checkout path is not.
  • Databases with aggressive connection pools you can't reconfigure — they'll never idle anyway, so you get the complexity and none of the benefit.
  • Anything where an alert storm on wake would be worse than the cost saving. If your on-call rotation gets paged by wake latency, you have made everyone's life worse to save a modest amount of money.

Where it's clearly right: fleets with a long idle tail, staging and preview databases, internal tools, and per-customer instances where most customers are asleep at any given moment. In those cases the compute saving is large — often most of the fleet — and the affected users are people who understand that something they open twice a month takes a moment to warm up.

The general principle, same as everywhere else in infrastructure: know your duty cycle, know your tail latency, and don't trade the second for the first on anything a customer is waiting on.

Frequently asked questions

How does suspending an idle Postgres database work?

The platform detects that there have been no queries for a configured period, quiesces Postgres cleanly so in-flight transactions finish and the cluster reaches a consistent state, then captures state and releases the compute while the durable volume holding the data directory persists. When a connection arrives afterwards, a proxy in front holds that connection open, restores the machine, waits for Postgres to accept connections, and completes the client's connection to the live database. Holding rather than refusing the connection is the key detail — it turns what would be an outage into first-query latency.

Why does my database never go idle?

Almost certainly a connection pool. Application frameworks typically open a pool at startup and keep those connections open indefinitely with keepalives, which looks to the database like a permanently connected client. If idle detection means no open connections, it will never trigger. The fix is to measure query activity rather than connection count, and to distinguish pool keepalive traffic from real queries. On the application side, configuring a pool idle timeout so unused connections actually close helps — most pool libraries support it and most teams never set it. Health checks that run a database query are the other common culprit.

What breaks in my application when the database suspends?

Mostly assumptions rather than code. The first connection after a wake is slow, so latency alerts will fire on every wake unless tuned. Health checks that query the database act as keepalives and prevent suspension entirely. Background jobs wake the database on their own schedule, which is correct but should be intentional. Connection retry logic needs to exist with sensible backoff, since a connect attempt during wake may fail once. And a suspended database emits no metrics, so dashboards show gaps and any alert configured to fire on missing data will need to treat suspension as an expected state.

Which databases should not be suspended?

Production databases carrying real traffic, because they never idle so there is no saving and only added wake-latency risk. Anything where first-query latency is customer-visible on a critical path such as checkout. Databases whose connection pools you cannot reconfigure, since they will never idle and you get the complexity without the benefit. And anything where wake-related alerts would page your on-call rotation, because making everyone's life worse to save a modest amount of money is a bad trade. Suspension is best for staging and preview databases, internal tools, and per-customer fleets with a long idle tail.

Does suspending a database stop all billing?

It should stop compute billing — memory and vCPU — while storage continues, correctly, because your data still occupies a durable volume. Ask any platform exactly which components stop. On the implementation side this is a place where bugs are common in both directions: over-billing means charging for a database that is not running, which customers notice and complain about, and under-billing is silent revenue loss. Both come from the same root cause, which is that usage windows must be finalised explicitly at every lifecycle transition — pause, wake, restart, migration, crash — rather than relying on a periodic sweep to notice later.

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.