Moving the database is the part of a Heroku migration that keeps people up at night — it is the one piece you cannot roll forward from if it goes wrong. This is the focused engineer playbook for Heroku Postgres → Amazon RDS (or Aurora PostgreSQL): which of the three cutover methods fits your database size, how to handle Heroku Postgres specifics (followers, connection limits, pinned extensions, forced SSL, the DATABASE_URL swap), the exact cutover runbook with a rollback, and how to compute your real downtime window. A MAP-funded AWS partner can run the whole cutover — often at little-to-no cost.
You can re-deploy a web app a hundred times in an afternoon; a botched build just rolls back. The database is different. It holds state, it accepts writes continuously, and once you have taken writes on the new instance you cannot quietly undo them. That asymmetry is why the Postgres move deserves its own runbook, separate from the rest of the Heroku migration.
This page is deliberately narrow. The companion playbook (Heroku → AWS) covers the whole move — dynos to ECS/Fargate or App Runner, Redis to ElastiCache, add-ons to AWS-native, buildpacks to containers. Here we go deep on the one component where a mistake is expensive and hard to reverse: getting every row of Heroku Postgres onto Amazon RDS, with the application pointed at it, inside a downtime window you have agreed in advance.
The core tension is between simplicity and downtime. The simplest method (dump and restore) requires a write freeze for as long as the dump-plus-restore takes — fine for a 5GB database at 2 a.m., unacceptable for a 600GB database serving customers across time zones. The near-zero-downtime methods (DMS CDC and logical replication) remove the freeze by continuously replicating live changes into RDS while the old database keeps serving, so the actual switch is just a brief pause to drain in-flight writes and repoint the app. The whole game is choosing the method that buys you exactly the downtime budget you have — and no more complexity than that requires.
Two facts shape every decision below. First, Heroku Postgres is a managed Postgres with guardrails — you do not get superuser, you cannot install arbitrary extensions, and connections are capped by plan — so a few migration techniques that work on self-managed Postgres are simply unavailable, which narrows the method choice in a helpful way. Second, the production cutover itself is almost anticlimactic: it is a single environment change, swapping DATABASE_URL so the app resolves to the RDS endpoint instead of Heroku Postgres. Everything else is preparation and verification around that one swap.
Almost every Heroku Postgres → RDS migration uses one of three methods. The decision is not about which is "best" in the abstract — it is about your database size and the downtime you can take. Pick the simplest method that fits both.
Read these as a decision tree, not a menu. Start at the top; drop to the next method only when the constraint above forces you to.
How it works: freeze writes (put the app in maintenance mode), run pg_dump against Heroku Postgres in the custom/directory format, stream it into pg_restore against the new RDS instance, verify, then bring the app up pointed at RDS. A common one-shot is pg_dump $HEROKU_DATABASE_URL -Fc | pg_restore --no-owner --no-acl -d $RDS_DATABASE_URL — the --no-owner / --no-acl flags matter because Heroku owns the source roles and you do not want to recreate them on RDS.
Downtime: the full dump-plus-restore time, because writes are frozen the entire time. Roughly 10–60 minutes for databases up to ~20GB on a fast link; longer for larger or heavily-indexed schemas (index rebuilds dominate restore time).
Best for: databases under ~20GB where a 15–60-minute maintenance window at an off-peak hour is acceptable. This is the overwhelming majority of early-stage apps, and you should not reach for anything fancier if this fits.
Watch out for: a logical dump is a point-in-time snapshot — any write after the dump starts is lost unless the app is genuinely frozen, so maintenance mode must be real (block writes, not just hide the UI). Restore on a generously-sized instance and dial it down afterward; CPU and IOPS during restore are far higher than steady state.
How it works: AWS Database Migration Service does an initial full load of the existing data into RDS, then switches to change data capture — it reads Heroku Postgres's write-ahead log and streams every subsequent INSERT/UPDATE/DELETE into RDS continuously. The new instance stays within seconds of the source while you run validation against it. At cutover you stop writes briefly, let CDC drain the last changes (replication lag → 0), then repoint the app.
Downtime: typically 0–10 minutes — only the time to drain final changes and swap DATABASE_URL, independent of database size. A 500GB database cuts over in the same short window as a 5GB one.
Best for: databases too large to freeze (dozens of GB to multiple TB) or apps that take writes around the clock. DMS also has built-in data validation that row-counts and checksums source vs target, which is reassuring on a big move.
Watch out for: CDC against PostgreSQL needs logical replication enabled on the source (Heroku exposes rds.logical_replication-style settings on its newer plans; confirm your plan supports it). DMS is excellent at bulk row movement but does not migrate sequences' current values, certain constraints, or large-object edge cases perfectly — reconcile sequences and re-validate constraints at cutover.
How it works: PostgreSQL's own publish/subscribe logical replication. You restore the schema to RDS, create a PUBLICATION on the source and a SUBSCRIPTION on RDS, and Postgres copies the initial data and then streams ongoing changes natively — no extra service in the path. Cutover is the same shape as DMS: drain lag to zero, repoint the app.
Downtime: 0–10 minutes, same as DMS.
Best for: homogeneous Postgres-to-Postgres moves (which a Heroku → RDS migration always is) where you want to avoid standing up DMS, and where source and target major versions are compatible. Engineers comfortable in psql often prefer this for its directness and the absence of a billed replication instance.
Watch out for: it depends on Heroku exposing logical-replication on your plan, and version skew matters (replicating into a much newer major version can surface incompatibilities). Logical replication does not copy sequence positions automatically and historically does not replicate DDL — freeze schema changes during the migration window and advance sequences manually at cutover.
If the database is small enough to freeze for an hour at 2 a.m., use pg_dump | pg_restore and stop. If it is not, use DMS CDC (managed, with built-in validation) or native logical replication (no extra service, engineer-friendly) to get a near-zero-downtime cutover. There is no prize for using the complex method on a database that did not need it.
Heroku Postgres is managed Postgres with guardrails. Those guardrails change how the migration behaves, and four of them catch teams who treat the source as if it were a vanilla self-hosted database.
Account for each of these before you start copying data — they are far cheaper to handle in planning than to discover mid-cutover.
For all the preparation, the production switch itself is one environment change. Understanding exactly how Heroku wires DATABASE_URL is what keeps that change from silently reverting on you.
On Heroku, the Postgres add-on is attached to the app, and that attachment is what sets the DATABASE_URL config var. The add-on also rotates credentials periodically, which means DATABASE_URL can change underneath you — and, critically, if you only overwrite the value while the add-on is still attached, a later rotation (or a config refresh) can stomp your new value and point the app back at Heroku Postgres. The fix is to detach the add-on from the app's automatic DATABASE_URL once you cut over, so nothing rewrites it.
The clean sequence: (1) put the app in maintenance mode to stop writes; (2) confirm replication lag is zero (DMS/logical replication methods) or the dump/restore has completed and verified (dump method); (3) reconcile sequences and re-validate constraints; (4) set DATABASE_URL to the RDS endpoint connection string — including ?sslmode=verify-full and the RDS CA — and remove Heroku's automatic attachment so it cannot be overwritten; (5) run smoke tests (a read, a write, a representative transaction) against RDS while still in maintenance; (6) take the app out of maintenance. Traffic now hits RDS.
A subtlety worth planning for: the application reads DATABASE_URL at boot for most frameworks (Rails, Django, Node connection pools), so changing the config var typically requires a restart/redeploy to take effect. Sequence the restart inside the maintenance window so there is never a moment where some processes are writing to Heroku Postgres and others to RDS — split-brain writes across two databases are the worst failure mode of a cutover, and a single coordinated restart is what prevents them.
Both are managed PostgreSQL on AWS and both are valid destinations from Heroku. RDS for PostgreSQL is the like-for-like landing spot; Aurora PostgreSQL is the upgrade you choose when you want better failover, storage that scales itself, and cheaper read scaling.
RDS for PostgreSQL runs the community PostgreSQL engine on managed infrastructure — you pick the exact major/minor version, the instance class, the storage type (gp3 is the sensible default), and whether to enable Multi-AZ for a synchronous standby. It is the closest mental model to Heroku Postgres and the simplest place to start: if your goal is "the same Postgres, but on AWS and cheaper at scale," RDS is the right answer and you can always move to Aurora later.
Aurora PostgreSQL is AWS's PostgreSQL-compatible engine with a re-architected storage layer. The practical wins: storage auto-grows in 10GB increments up to 128TB with no manual resizing, replication is to a shared distributed storage volume so read replicas are cheap and failover is typically faster (often well under a minute), and you can add up to 15 low-lag readers for read scaling. The trade is a modest premium on compute and storage-I/O billing, and slightly less control over the exact engine internals. For a Heroku app that already leans on followers for reads or expects rapid data growth, Aurora often pays for itself; for a steady mid-size workload, RDS is perfectly sufficient.
Because both speak the PostgreSQL wire protocol and SQL, the migration mechanics above are identical for either target — pg_dump/restore, DMS CDC, and logical replication all land equally well on RDS or Aurora. The target decision is therefore an architecture choice you can make on its own merits (failover SLA, growth trajectory, read-scaling needs, budget), not a constraint imposed by the migration method.
A database cutover is a sequenced operation with a rehearsed rollback, not a vibe. Write it down, dry-run it against a staging copy, and have the rollback path decided before you touch production.
A representative near-zero-downtime runbook (DMS CDC or logical replication), in order:
Rollback is only safe before the app takes writes on RDS. Until you exit maintenance, rolling back is trivial — revert DATABASE_URL to Heroku Postgres and restart. After writes land on RDS, you cannot simply switch back without losing those writes; reverse replication (RDS → Heroku) would have to have been set up in advance, which almost no one does. So the real decision gate is the smoke test at minute 6–9: do not exit maintenance until you are confident, because that is the last cheap exit.
Teams either over-promise ("zero downtime!") or panic ("we need an 8-hour window!"). Both come from not doing the arithmetic. Here is how to estimate the real number for each method so you can commit to a window you can actually hit.
For the dump-and-restore method, downtime ≈ dump time + transfer time + restore time, all serialized because writes are frozen throughout. Dump and restore are both dominated by data volume and, on restore, by index rebuilds — a useful planning rule is that restore (with index creation) often takes longer than the dump, and total wall-clock runs roughly an hour per ~15–25GB on a healthy link and instance, though heavily-indexed schemas push that up. The only reliable number is the one you measure: restore a recent dump to a throwaway RDS instance and time it. That measured number, plus a safety margin, is your window.
For the near-zero-downtime methods (DMS CDC, logical replication), the full-load and catch-up happen before the window with the app still live, so they do not count as downtime at all. Your actual downtime is only: time to drain final replication lag (seconds-to-low-minutes if you cut over during low traffic) + sequence/constraint reconciliation + the app restart to pick up the new DATABASE_URL + smoke tests. That total is typically 5–10 minutes and, crucially, is roughly constant regardless of database size — which is the entire reason these methods exist.
Two levers shrink the window further. First, cut over during your genuine traffic trough (for a US-centric SaaS, that is usually the early-morning ET hours) so there is less in-flight write to drain and less risk during the restart. Second, rehearse the exact sequence against a staging cutover at least once; the difference between a 6-minute and a 40-minute real cutover is almost always whether someone is improvising. The downtime you should publish to stakeholders is your rehearsed time plus margin — not your best-case hope.
You can run a Heroku Postgres → RDS cutover yourself with the methods above. Many teams would rather have someone who has done it dozens of times own the runbook, the validation, and the 6 a.m. switch — especially when AWS will fund it.
CloudRoute routes you to a vetted AWS partner who runs the database cutover end-to-end: provisioning RDS or Aurora, choosing the method that fits your size and downtime budget, standing up the replication, building and rehearsing the runbook, executing the cutover in your agreed window, and owning the rollback decision gate. You keep your engineers on product instead of becoming part-time DBAs for a fortnight.
The funding mechanism is the part most teams do not know about. The AWS Migration Acceleration Program (MAP) funds qualifying migrations — AWS typically covers the assessment phase and credits a meaningful share of the migration cost, with the partner paid through MAP — so a qualifying migration can cost the customer little-to-nothing. A standalone Postgres move is usually small on its own; it most often qualifies as part of the broader Heroku → AWS migration (app + database + supporting services), which is exactly the kind of workload MAP is designed to fund. Honest framing: MAP applies to qualifying migrations (typically with a reasonable post-migration AWS-spend commitment); where it does not, CloudRoute is still a vetted-partner referral that de-risks the cutover — the same outcome without the funding.
Either way, the customer is never in the payment loop with CloudRoute: AWS funds the credits and the MAP engagement, the partner delivers the migration, and CloudRoute is paid by the partner. If you are eyeing this database move as the first step of leaving Heroku, the funded path means the riskiest piece is run by specialists and a large share of the bill is on AWS.
The same three methods, side by side on the variables that actually decide which you use: downtime, the database size each suits, and the risk/effort profile. Pick the simplest row that fits your size and downtime budget.
| Method | Downtime | DB size it suits | Near-zero downtime? | Risk / effort | When to use it |
|---|---|---|---|---|---|
| pg_dump | pg_restore | Full dump + restore time (≈10–60 min for ≤20GB) | Small (≤ ~20GB) | No — writes frozen throughout | Low effort, low complexity; risk = data lost if maintenance isn't real | Small DB + an off-peak maintenance window you can take |
| AWS DMS + CDC | 0–10 min (drain lag + repoint) | Large / busy (dozens of GB to multi-TB) | Yes | Medium effort; managed service with built-in validation; reconcile sequences/constraints at cutover | Large or 24/7 DB needing near-zero downtime, want managed tooling + validation |
| Native logical replication | 0–10 min (drain lag + repoint) | Medium–large, PG-to-PG | Yes | Medium effort; no extra billed service; version skew + manual sequences/DDL freeze to watch | Homogeneous PG move, engineer comfortable in psql, want no extra service in the path |
Situation: The database was too large to freeze — customers wrote around the clock across US and EU hours, so a dump/restore window was a non-starter. The team had tried to scope a near-zero-downtime cutover internally but stalled on the Heroku specifics: logical-replication eligibility on their plan, matching a pinned extension set (PostGIS + pg_stat_statements + citext), carrying their PgBouncer pooling over, and a rollback they actually trusted. Nobody wanted to own the 6 a.m. switch on a 420GB production database.
What CloudRoute did: Routed within 20 hours to a US-East partner with a database-migration track record. The partner chose AWS DMS with CDC (for the built-in data validation on a big move), landed the data on Aurora PostgreSQL for faster failover and cheap read replicas to replace the two Heroku followers, adopted RDS Proxy in place of PgBouncer, rehearsed the runbook twice against a staging cutover, and scoped it inside the team's broader Heroku → AWS migration so it qualified for MAP. Cutover ran in the early-morning ET trough.
Outcome: Final cutover downtime: 7 minutes (drain lag → reconcile sequences → swap DATABASE_URL + restart → smoke test). Zero data loss, validated by DMS row-count/checksum + the app suite. Heroku Postgres held read-only for 72h as a rollback target, then retired. Because the move was part of a qualifying MAP migration, AWS funded the engagement and credited the migration cost — the customer's out-of-pocket was effectively $0; CloudRoute was paid by the partner.
engagement window: ~3 weeks · cutover downtime: 7 min · data loss: 0 · method: DMS CDC → Aurora · cost to customer: ~$0 (MAP-funded)
CloudRoute routes you to a vetted AWS partner who picks the right method, rehearses the runbook, and owns the switch — and when the migration qualifies for MAP, AWS funds it. Customer pays little-to-nothing.