# Migrate From Heroku Postgres: Dump, Cutover, Rollback

> Three ways to move a Heroku Postgres database, how to size the downtime, a cutover that doesn't lose writes, and the restore errors Heroku dumps throw.

1. [Home](/)
2. [Blog](/blog)
3. Migrating Off Heroku Postgres: The Dump, the Cutover and the Rollback

DeploymentSeptember 26, 202612 min read

# Migrating Off Heroku Postgres: The Dump, the Cutover and the Rollback

Three ways to get a database out of Heroku Postgres, how to work out the downtime before you commit to it, a cutover order that doesn't lose writes, and the point after which rolling back stops being free.

## The short version

Under 20 GB, use heroku pg:backups:capture and download, then pg\_restore with --no-owner and --no-acl. Above that, run pg\_dump yourself. Maintenance mode alone does not stop writes, because worker dynos and Scheduler keep running, so scale them to zero first. Rollback is free until the first write lands in the new database.

[Teo Marquardt](/about#author)· Facts last checked September 26, 2026

Moving an app off Heroku is mostly configuration. The code goes wherever a container runs, the environment variables get copied across, the Procfile turns into settings. All of that can be done twice if the first attempt goes wrong. The database can't. It's the one part of the move where a mistake costs data rather than an afternoon, and it's the part the platform migration guides spend the least time on.

This page is only about that part: getting the data out of Heroku Postgres, putting it somewhere else, and switching over without losing the writes that arrive in between. The rest of the move (the variables Heroku set for you, the Procfile, the build) is covered in [choosing a European Heroku alternative](/blog/european-heroku-alternative), along with where to go. The commands below are Heroku's own and standard PostgreSQL tools, so they work whatever the destination is.

## Is Heroku still a thing?

Yes, and nobody is being forced off it. On 6 February 2026 Salesforce [stopped selling Heroku Enterprise contracts to new customers](https://siliconangle.com/2026/02/06/salesforce-stop-selling-enterprise-heroku-subscriptions-scale-back-upgrades/) and moved the platform into what it calls sustaining engineering: security, stability and support continue, new features don't. Existing customers can keep using it and renewing at the same prices, and there is no end-of-life date.

So there's no deadline. What changed is that staying is now a decision about a platform that will look the same in three years as it does today. For some teams that's fine. For others it's the push to do a migration they'd been putting off, and those teams have time to do it properly, which mostly means rehearsing the database step.

## Three ways to get your data out of Heroku Postgres

Which one fits depends on two numbers: the size of the database and how long the app can stop accepting writes. Everything else follows from those.

| Method                                              | Size it suits                        | Downtime                                     | What to watch                                                           |
| --------------------------------------------------- | ------------------------------------ | -------------------------------------------- | ----------------------------------------------------------------------- |
| heroku pg:backups:capture \+ download + pg\_restore | Up to about 20 GB                    | Dump, transfer and restore time              | Heroku says it can time out on busy or larger databases                 |
| pg\_dump directly against DATABASE\_URL             | Any size                             | Same, but you control format and parallelism | Run it from a machine close to the database, not over a home connection |
| Trigger-based replication (e.g. Bucardo)            | Large, or low tolerance for downtime | Minutes at the switch                        | Heroku doesn't offer native logical replication out; more moving parts  |

Methods for moving a Heroku Postgres database, per Heroku's Dev Center and help articles as of 26 September 2026.

### 1\. heroku pg:backups:capture, then download

This is the path Heroku [documents for exports](https://devcenter.heroku.com/articles/heroku-postgres-import-export). PGBackups takes a logical backup in `pg_dump`'s custom format, which is compressed and restores in parallel. Heroku's own guidance is to [use it for moderately loaded databases up to 20 GB](https://devcenter.heroku.com/articles/heroku-postgres-backups); beyond that, or with many schemas or large objects, the capture can time out.

bash

```
# Take a fresh backup and download it
heroku pg:backups:capture --app your-app
heroku pg:backups:download --app your-app   # writes latest.dump

# Or get a signed URL and pull it from a server near the destination
# (the URL expires after 60 minutes)
heroku pg:backups:url --app your-app
```

The `pg:backups:url` variant is the one to use if the destination database is far from your laptop. Download the file on a machine in the same region as the new database, and the transfer stops being the slowest step.

### 2\. pg\_dump directly

For anything past the 20 GB guidance, or when you want control over the format, run `pg_dump` yourself against the connection string. Directory format with `--jobs` dumps several tables at once, and `pg_restore` can read it back in parallel too. Use a `pg_dump` at least as new as the server version, and read `DATABASE_URL` at the moment you run it rather than copying it into a script: Heroku rotates Postgres credentials, so a saved one can go stale.

bash

```
# Parallel dump in directory format
pg_dump "$(heroku config:get DATABASE_URL --app your-app)" \
  --format=directory --jobs=4 --no-owner --no-acl \
  --file=heroku_dump/
```

### 3\. Replicate, then switch

If the database is large and the app can only stop for a few minutes, dump-and-restore won't fit in the window. Heroku's help center says it [doesn't support logical replication with external instances](https://help.heroku.com/TVS8OHTR/does-heroku-postgres-support-logical-replication) on Heroku Postgres, so the native Postgres route isn't available. Teams that needed near-zero downtime have used trigger-based replication instead. Getsafe [wrote up a move with Bucardo](https://medium.com/hellogetsafe/pulling-off-zero-downtime-postgresql-migrations-with-bucardo-and-terraform-1527cca5f989). It works, and it adds triggers to every replicated table on the production database, so rehearse it on a copy first. Most small and mid-sized apps don't need it: a measured maintenance window of half an hour is cheaper than a replication setup.

## How long the downtime will actually be

The honest answer is that nobody can tell you without a rehearsal, and the rehearsal is cheap. Take a backup now, restore it into the destination, and time each step. The downtime on the day is roughly the sum of those times plus the checks you run before reopening.

* Capture or dump time. Grows with data size and with load on the database while it runs.
* Transfer time. Near zero if you download on a server close to the destination, and the slowest step if you don't.
* Restore time. Usually longer than the dump, because `pg_restore` rebuilds every index and re-checks every constraint after loading the rows. `--jobs` helps a lot here.
* Verification. Row counts on your main tables, a login, a write, a read. Budget ten minutes and write the checks down in advance.

Rehearse on a copy that's about the same size as production, not on a seed database. Restore time doesn't scale neatly with size, because a few big indexes can dominate it, and those only show up with real data. Add a margin to whatever you measured, and put that number in the maintenance notice.

## The restore errors a Heroku dump will throw

Heroku dumps carry a few things that only exist on Heroku, and the restore will complain about them. Two of them look alarming and aren't.

### ERROR: schema "\_heroku" does not exist

Backups from Essential-tier databases include event triggers that call functions in Heroku's internal `_heroku` schema. Restoring elsewhere produces `pg_restore: error: could not execute query: ERROR: schema "_heroku" does not exist`, repeated for each trigger. Heroku [says these are internal and don't affect your data](https://help.heroku.com/HYME3MD5/unexpected-statements-in-the-backup-from-a-essential-x-database-is-causing-local-pg%5Frestore-to-fail), and recommends filtering them out with a restore list:

bash

```
pg_restore -l latest.dump > unfiltered.list
sed -e '/EVENT TRIGGER/ s/./;&/' unfiltered.list > filtered.list
pg_restore --use-list=filtered.list --no-owner --no-acl \
  --jobs=4 -d "$NEW_DATABASE_URL" latest.dump
```

### schema "heroku\_ext" does not exist

Heroku installs extensions such as `pg_stat_statements` or `pgcrypto` into a `heroku_ext` schema, and the dump creates them there. On a database that has no such schema, the extension statements fail. The simplest fix is to create the schema on the destination before restoring (`CREATE SCHEMA IF NOT EXISTS heroku_ext;`). Then check with `\dx` that every extension your app uses exists on the new side, in a version it supports. If your schema qualifies functions with `heroku_ext.`, the schema has to stay; if nothing references it, you can move the extensions to `public` after the cutover.

### Ownership and grants

Heroku's database user names are generated, and the destination's won't match. That's what `--no-owner` and `--no-acl` are for: every object ends up owned by the user you restore as, and none of Heroku's grants come along. If your app relies on a separate read-only role, recreate it on the destination and grant it explicitly.

## The cutover, in order

The one thing to know before writing the runbook: [maintenance mode doesn't stop writes](https://devcenter.heroku.com/articles/maintenance-mode). It blocks incoming HTTP requests to web dynos and serves a static page. Heroku's docs are explicit that worker dynos keep running and that Scheduler jobs can still run. A queue worker draining jobs during your final dump writes rows the dump doesn't have, and those rows are lost at the switch. Stop them yourself.

bash

```
heroku maintenance:on --app your-app
# web dynos stop receiving requests; workers and Scheduler do not stop
heroku ps:scale worker=0 --app your-app
```

1. A day or more before: lower the TTL on any DNS record pointing at Heroku, so the switch propagates in minutes. Deploy the app on the new platform against a restored copy and make sure it boots.
2. Put up the notice and run `heroku maintenance:on`.
3. Stop everything else that writes: `heroku ps:scale worker=0` for each non-web process type, and disable the Heroku Scheduler jobs.
4. Take the final backup with `heroku pg:backups:capture` (or run the final `pg_dump`).
5. Restore into the new database with the filtered list, `--no-owner`, `--no-acl` and `--jobs`.
6. Run the checks you wrote down: row counts on key tables, sequences, extensions, a login and a write.
7. Point the new app's `DATABASE_URL` at the new database, start its workers and scheduler, switch DNS.
8. Leave the Heroku app in maintenance mode, with its database untouched, until the rollback window closes.

Take a backup on the new side straight away

Once the restore finishes and the checks pass, take an on-demand backup of the new database before opening traffic. If something goes wrong in the first hour, you want a known-good snapshot that already lives on the destination, not a file on someone's laptop.

### What happens to heroku run rails db:migrate

On Heroku, schema migrations usually run from the Procfile's release phase (`release: rails db:migrate`, `release: python manage.py migrate` and so on) or by hand with `heroku run`. After the move, that line needs a home on the new platform, usually a pre-deploy or release command that runs once per deploy before the new version takes traffic. Don't run it during the cutover itself. Ship the database as it is, confirm the app works on it, and let the next normal deploy run the next migration. Why migrations in a rolling release have to work with both the old and the new code is covered in [how git push to deploy works](/blog/git-push-to-deploy).

## The rollback you want to have written down

Until the first real write lands in the new database, rolling back costs nothing: point DNS back at Heroku, scale the workers up, run `heroku maintenance:off`. The Heroku database is exactly as you left it, because nothing wrote to it.

After the first write, that stops being true. Every order, signup or message created on the new side exists only there, and rolling back now means dumping those rows and bringing them back to Heroku, which is a second migration in the opposite direction. So decide in advance how long you'll watch before calling it done (an hour of normal traffic is a reasonable default), what would make you roll back in that time, and who makes the call. Keep the Heroku database for a while after that, downgraded if you want to save money, but don't delete it the same week.

## How Runsite handles it

If the destination is Runsite, the database side is ordinary PostgreSQL and needs nothing special. Every [managed PostgreSQL](/services/postgresql) database has an external connection URL with TLS, so `pg_restore` runs against it from your laptop or a server like it would against any other Postgres. It's backed up automatically every day, and the Backups tab has a Backup now button for the snapshot the callout above recommends. The entry Nano plan is €3/mo (0.1 vCPU, 256 MB, 1 GB of storage), and larger plans go up to 8 vCPU and 16 GB on Business.

What it doesn't have: a Heroku importer, and point-in-time recovery. A restore on Runsite rolls the whole database back to a snapshot, so keep the Heroku database as your fallback during the rollback window. Everything runs in Germany, nothing replicates outside the EU, and a signed [data processing agreement](/blog/what-is-a-data-processing-agreement-dpa) comes with every account. How to connect a service to the database is in the [Runsite docs](https://docs.runsite.app).

## The short version

Heroku isn't going away on a date, so there's time to do this properly. Under 20 GB, `heroku pg:backups:capture` and `pg:backups:download`, then `pg_restore --no-owner --no-acl --jobs`. Above that, `pg_dump` in directory format. For a very large database with almost no tolerance for downtime, trigger-based replication, because Heroku doesn't offer logical replication out.

Rehearse on a full-size copy and time it. Expect the `_heroku` and `heroku_ext` errors and handle them before the day. On the day, maintenance mode isn't enough: scale workers to zero and turn off Scheduler before the final dump. And write down when rollback stops being free, which is the moment the first real write reaches the new database.

Related service

## PostgreSQL on Runsite

Deploy from a single git push and keep active apps warm — no cold starts, hosted entirely in the EU with a signed GDPR DPA on every plan.

[Explore PostgreSQL](/services/postgresql)

[Back to all articles](/blog)

FAQ

## Frequently Asked Questions

Common questions about this service.

### Is Heroku still a thing?

Yes. Heroku is still running and still sells its standard plans. On 6 February 2026 Salesforce stopped selling Heroku Enterprise contracts to new customers and moved the platform into sustaining engineering, which means security, stability and support continue but no new features are being developed. Existing customers can keep using it and renew at the same prices, and as of 26 September 2026 no end-of-life date has been announced.

### What happened with Heroku?

Two things, four years apart. In November 2022 Heroku removed its free dynos, free Postgres and free Redis, which pushed hobby projects to other platforms. In February 2026 Salesforce, which owns Heroku, announced it would stop developing new features and stop selling Enterprise contracts to new customers, keeping the platform running in sustaining mode. According to SiliconANGLE's report of the announcement, Salesforce is redirecting product and engineering investment toward areas including AI. Existing apps are unaffected in the short term, but teams planning for the next few years are treating it as a signal to schedule a migration.

### Why is Heroku no longer free?

Heroku ended its free tier on 28 November 2022, citing the cost of running it and abuse of free resources. Free dynos, free Heroku Postgres and free Heroku Data for Redis plans were removed. The cheapest options since then are Eco dynos at $5/mo for a shared pool of hours, Basic dynos at $7/mo, Heroku Postgres Essential-0 at $5/mo and the Mini Key-Value Store at $3/mo, as listed on Heroku's pricing page on 26 September 2026.

### What is the best way to migrate a database?

Rehearse it before you do it for real. Take a backup of the production database, restore it into the destination, and time each step, because the restore is usually slower than the dump and the only way to know the downtime is to measure it on data of the real size. On the day, stop every process that writes (web traffic, background workers and scheduled jobs), take the final dump, restore it, verify row counts and a test write, then switch the application over. Keep the old database untouched until you're sure you won't roll back, since rollback is free only until the first write lands on the new side. For Postgres specifically, restore with --no-owner and --no-acl so user names and grants from the old host don't carry over.

Keep reading

## Related articles

[Deployment15 min read8 European Heroku Alternatives in 2026 — and How to Check One Really Is EuropeanEight Heroku alternatives operated by European companies, checked on the column the lists leave out: which company you contract with, and where it is registered.Aug 7, 2026Read](/blog/european-heroku-alternative)[Databases7 min readHow to Automate PostgreSQL Backups to S3 on a ScheduleA managed database backs itself up daily — but a second, portable copy in your own S3 bucket is yours to keep. Here's how to schedule pg\_dump to object storage with a cron job, and when it's worth doing.Jun 22, 2026Read](/blog/postgresql-backup-to-s3)[Deployment12 min readPaaS Pricing Comparison 2026: One App, Priced on Seven PlatformsThe same small production app (a web service, Postgres, Redis and 100 GB of traffic) priced on Render, Heroku, Railway, Fly.io, DigitalOcean, Vercel and Runsite, for one developer and for a team of three.Sep 26, 2026Read](/blog/paas-pricing-compared)

## Your app deserves to be online

€5 of credit on signup. Deploy in under a minute. No credit card needed.

[Start deploying](https://dashboard.runsite.app/login)[View documentation](https://docs.runsite.app)

---

Source: https://runsite.app/blog/migrate-from-heroku
