Postgres Ops
Keep a production Postgres safe to lose and fast to serve: backups you have actually
restored, recovery you have actually tested, and pooling that survives serverless
connection storms. Restore and recovery operations can overwrite data — they run
behind explicit confirmation, against the right target.
Contract
Inputs:
- A Postgres instance (managed or self-managed); mode
backup / restore /
pooling / dr.
Outputs:
- A backup/PITR plan or verification, a restore result, a pooling configuration, or a
DR runbook.
Creates/Modifies:
restore writes to a database. Never to production without explicit, named
confirmation of the target. backup/pooling/dr produce config and plans.
External Side Effects:
- Reads DB metadata;
restore mutates a database. Treat connection strings and dumps
as secrets — never print credentials.
Confirmation Required:
- Before any restore, and again if the target is production.
- Before changing live pooling config that can drop connections.
Delegates To:
aws-infrastructure for the storage/IAM around backups on AWS.
monitoring-setup to alert on replication lag, connection saturation, and slow
queries.
Backup (backup)
Match the method to the hosting:
Managed (RDS, Prisma Postgres, etc.) — enable automated snapshots + PITR in the
provider; set retention to the business's tolerance. Verify it is on rather than
assuming; snapshots you never checked are not a backup.
Self-managed — logical backups for portability, physical + WAL for PITR:
pg_dump --format=custom --file=db-$(date +%F).dump "$DATABASE_URL" # logical
# physical base backup + WAL archiving enables point-in-time recovery
pg_basebackup -D /backup/base -Ftar -z -X stream
Two rules that decide whether a backup is real: store it off the database host
(a snapshot on the same disk dies with the disk), and set a retention that covers the
time to notice a problem, not just to react.
Point-in-time recovery
PITR needs continuous WAL archiving (archive_mode = on, archive_command shipping
WAL off-host) on top of a base backup. Define and write down two numbers:
- RPO (how much data you can lose) → drives backup/WAL frequency.
- RTO (how long recovery may take) → drives whether a warm standby is needed.
Recovery replays WAL from a base backup up to a target time. The only proof it works
is a drill (below).
Restore & verify (restore)
A backup is unproven until restored. Restore into a scratch database, never over
production, and verify:
scratch_db="restore_check_$(date +%s)"
trap 'dropdb --if-exists "$scratch_db"' EXIT
createdb "$scratch_db"
pg_restore --dbname="$scratch_db" --clean --if-exists db-YYYY-MM-DD.dump
psql "$scratch_db" -c "SELECT count(*) FROM <critical_table>;" # sanity-check row counts
dropdb "$scratch_db"
trap - EXIT
Only restore to production on an explicit, named request, after confirming the target
connection string is the intended one. Schedule a periodic restore drill — an
untested backup is a guess.
Connection pooling (pooling)
Serverless (Vercel functions) + Postgres = connection exhaustion: each concurrent
invocation opens a connection, and Postgres has a hard max_connections. Put a pooler
in front:
- PgBouncer (or the provider's pooler) in transaction mode for serverless.
- Point Prisma at the pooled port for the app; use the direct connection only
for migrations (
directUrl in Prisma).
- Size the pool to the DB's
max_connections minus headroom for admin/migrations —
not higher, or the pooler just moves the exhaustion.
Disaster recovery (dr)
Produce a runbook someone can follow at 3am without this context:
- Detect — the alert that fires (from
monitoring-setup): replication lag,
connection saturation, disk full, instance down.
- Assess — data loss (last good backup/WAL) vs availability (standby present?).
- Recover — promote a standby, or restore latest base + replay WAL to just before
the incident.
- Verify — row counts on critical tables, app reconnects, no partial writes.
- Post-mortem — what to change so it does not recur (retention, alert threshold,
standby).
State the current RPO/RTO honestly, and name the gap between them and the target.
Anti-Patterns
- Backups on the same host/disk as the database — they die together. Off-host or
it is not a backup.
- Never restoring — an untested backup is a guess; drill it on a schedule.
- Restoring over production to "check" — always a scratch database first.
- No pooler in front of serverless — connection storms exhaust
max_connections
and take the app down under load.
- Printing connection strings or dump contents — they are secrets.
1---
2name: postgres-ops
3description: Operate a production Postgres database — backup strategy, point-in-time recovery, restore drills, connection pooling for serverless, and a disaster-recovery runbook. Use when asked to set up Postgres backups, plan disaster recovery, configure connection pooling, restore a database, or harden Postgres for production. Covers managed (RDS / Prisma Postgres) and self-managed instances.
4disable-model-invocation: true
5argument-hint: "[backup | restore | pooling | dr]"
6compatibility: Requires psql / pg_dump / pg_restore for self-managed instances; provider CLI for managed.
7allowed-tools: Bash(psql *) Bash(pg_dump *) Bash(pg_restore *) Bash(pg_basebackup *) Bash(createdb *) Bash(dropdb *)
8metadata:
9 version: "1.0.1"
10 tags: "postgres, backup, disaster-recovery, pooling, database, infrastructure"
11 author: Ship Shit Dev
12when_to_use: "postgres backups, database backup strategy, point in time recovery, PITR, restore the database, connection pooling, PgBouncer, disaster recovery, DR runbook, harden postgres"
13---
14
15# Postgres Ops
16
17Keep a production Postgres safe to lose and fast to serve: backups you have actually
18restored, recovery you have actually tested, and pooling that survives serverless
19connection storms. Restore and recovery operations can overwrite data — they run
20behind explicit confirmation, against the right target.
21
22## Contract
23
24Inputs:
25
26- A Postgres instance (managed or self-managed); mode `backup` / `restore` /
27 `pooling` / `dr`.
28
29Outputs:
30
31- A backup/PITR plan or verification, a restore result, a pooling configuration, or a
32 DR runbook.
33
34Creates/Modifies:
35
36- `restore` writes to a database. Never to production without explicit, named
37 confirmation of the target. `backup`/`pooling`/`dr` produce config and plans.
38
39External Side Effects:
40
41- Reads DB metadata; `restore` mutates a database. Treat connection strings and dumps
42 as secrets — never print credentials.
43
44Confirmation Required:
45
46- Before any restore, and again if the target is production.
47- Before changing live pooling config that can drop connections.
48
49Delegates To:
50
51- `aws-infrastructure` for the storage/IAM around backups on AWS.
52- `monitoring-setup` to alert on replication lag, connection saturation, and slow
53 queries.
54
55## Backup (`backup`)
56
57Match the method to the hosting:
58
59- **Managed (RDS, Prisma Postgres, etc.)** — enable automated snapshots + PITR in the
60 provider; set retention to the business's tolerance. Verify it is on rather than
61 assuming; snapshots you never checked are not a backup.
62- **Self-managed** — logical backups for portability, physical + WAL for PITR:
63
64 ```bash
65 pg_dump --format=custom --file=db-$(date +%F).dump "$DATABASE_URL" # logical
66 # physical base backup + WAL archiving enables point-in-time recovery
67 pg_basebackup -D /backup/base -Ftar -z -X stream
68 ```
69
70Two rules that decide whether a backup is real: store it **off the database host**
71(a snapshot on the same disk dies with the disk), and set a retention that covers the
72time to *notice* a problem, not just to react.
73
74## Point-in-time recovery
75
76PITR needs continuous WAL archiving (`archive_mode = on`, `archive_command` shipping
77WAL off-host) on top of a base backup. Define and write down two numbers:
78
79- **RPO** (how much data you can lose) → drives backup/WAL frequency.
80- **RTO** (how long recovery may take) → drives whether a warm standby is needed.
81
82Recovery replays WAL from a base backup up to a target time. The only proof it works
83is a drill (below).
84
85## Restore & verify (`restore`)
86
87A backup is unproven until restored. Restore into a **scratch** database, never over
88production, and verify:
89
90```bash
91scratch_db="restore_check_$(date +%s)"
92trap 'dropdb --if-exists "$scratch_db"' EXIT
93createdb "$scratch_db"
94pg_restore --dbname="$scratch_db" --clean --if-exists db-YYYY-MM-DD.dump
95psql "$scratch_db" -c "SELECT count(*) FROM <critical_table>;" # sanity-check row counts
96dropdb "$scratch_db"
97trap - EXIT
98```
99
100Only restore to production on an explicit, named request, after confirming the target
101connection string is the intended one. Schedule a **periodic restore drill** — an
102untested backup is a guess.
103
104## Connection pooling (`pooling`)
105
106Serverless (Vercel functions) + Postgres = connection exhaustion: each concurrent
107invocation opens a connection, and Postgres has a hard `max_connections`. Put a pooler
108in front:
109
110- **PgBouncer** (or the provider's pooler) in **transaction** mode for serverless.
111- Point Prisma at the **pooled** port for the app; use the **direct** connection only
112 for migrations (`directUrl` in Prisma).
113- Size the pool to the DB's `max_connections` minus headroom for admin/migrations —
114 not higher, or the pooler just moves the exhaustion.
115
116## Disaster recovery (`dr`)
117
118Produce a runbook someone can follow at 3am without this context:
119
1201. **Detect** — the alert that fires (from `monitoring-setup`): replication lag,
121 connection saturation, disk full, instance down.
1222. **Assess** — data loss (last good backup/WAL) vs availability (standby present?).
1233. **Recover** — promote a standby, or restore latest base + replay WAL to just before
124 the incident.
1254. **Verify** — row counts on critical tables, app reconnects, no partial writes.
1265. **Post-mortem** — what to change so it does not recur (retention, alert threshold,
127 standby).
128
129State the current RPO/RTO honestly, and name the gap between them and the target.
130
131## Anti-Patterns
132
133- **Backups on the same host/disk as the database** — they die together. Off-host or
134 it is not a backup.
135- **Never restoring** — an untested backup is a guess; drill it on a schedule.
136- **Restoring over production to "check"** — always a scratch database first.
137- **No pooler in front of serverless** — connection storms exhaust `max_connections`
138 and take the app down under load.
139- **Printing connection strings or dump contents** — they are secrets.