Skip to content

Migrating from Heroku Postgres to Layerbase

10 min readHerokuPostgreSQLValkey

Short version: your Heroku Postgres is upstream Postgres, so the database move is a read-once copy and one config var. Read DATABASE_URL from the app right before you start, because a stale copy from your notes is the single most common way this goes wrong. If you also use the Key-Value Store add-on, that is a second pass and it lands on Valkey, which is what the add-on has been running since Heroku renamed it. Your dynos, add-ons, and pipelines are not part of this. Only the data moves.

This is the how-to. If you are still deciding whether to move at all, Heroku Postgres alternatives makes the case both ways first.

What moves and what does not

The database moves. That is schema, indexes, constraints, rows, sequences, views, and functions, because both ends are Postgres and there is no proprietary wire protocol in between.

Your application does not move. Dynos keep running on Heroku, deploys keep working the way they do now, and pipelines and review apps are untouched. The only thing that changes in your app is which host it opens a connection to. That is deliberate: doing the database and the platform in the same weekend is how people end up unable to tell which change broke things.

Other add-ons do not come along either. If you run Kafka, a mail add-on, a logging drain, or the Scheduler, those stay exactly where they are. We host databases, not the rest of the platform. Make a list of your add-ons before you start so nothing surprises you at cutover.

Get DATABASE_URL fresh

Do not use a connection string you saved. Read it from the app, right now, in the terminal:

bash
heroku config:get DATABASE_URL -a your-app-name

The config var is the source of truth for what your app is currently connecting to, and a copy you pasted into a note last month may no longer be the live credential. If the import fails to authenticate, this is the first thing to re-check, and it is nearly always the answer.

The same rule applies to the Key-Value Store:

bash
heroku config:get REDIS_URL -a your-app-name

Treat both strings as secrets while you are working. They carry the password in the URL.

Copy the Postgres database

Paste the string on the Heroku migration page. You sign in, pick Postgres as the target, paste the DATABASE_URL, and we connect once, copy the schema and data into a fresh managed Postgres database, verify the row counts against your source, and hand back a report naming anything that did not come across. Your Heroku database is read and never written to. It keeps serving your app the whole time.

The manual path is the standard one, if you would rather drive it:

bash
pg_dump "$(heroku config:get DATABASE_URL -a your-app-name)" \
  --no-owner --no-privileges \
  | psql "postgresql://layerbase:<password>@your-host.cloud.layerbase.dev:5432/app?sslmode=require"

--no-owner --no-privileges matters here. Heroku's role names do not exist on the destination, and without those flags the restore spends its time complaining about grants it cannot apply.

Connections and quiet windows

Heroku's Essential plans are tight on connections: essential-0 and essential-1 allow 20, essential-2 allows 40, and standard-0 allows 200 (verified as of late August 2026 from Heroku Elements and the Heroku Postgres plans documentation). A dump takes one connection, so on paper you have room.

In practice, on a 20-connection plan with a web dyno and a worker already holding a pool open, one long-running read is enough to make a deploy or a traffic spike hit the ceiling. Run the copy when the app is quiet. If you cannot find a quiet window, scale the worker down for the duration rather than gambling on headroom you have not measured.

Take the copy as late as you reasonably can, too. Everything written to Heroku after the dump starts is not in the dump.

The Key-Value Store pass

Heroku renamed Heroku Redis to Key-Value Store on 11 October 2024, and the product runs Valkey: 9.0 by default, with 8.1 available, and 7.2 deprecated with an end of life of 19 December 2026 (Dev Center).

That is convenient, because Valkey is what we host too. You are not converting between engines here, you are moving a Valkey dataset from one host to another. Same commands, same data structures, same TTL semantics, and your client library does not change.

Two things about the source are worth knowing before you start.

The obvious export is blocked, and the useful one is not. Heroku blocks SAVE, BGSAVE, REPLICAOF, and CONFIG, and does not offer an RDB file to download. So the "just take a snapshot" approach you would use on a self-hosted instance is not available. What is not blocked is SCAN, DUMP, and RESTORE, and that is exactly how our import works: a non-blocking scan walks the keyspace, each key is dumped and restored on the other side with its type and TTL intact, and the source is never asked to do anything it will refuse.

The Mini plan does not persist data. Key-Value Store Mini is $3 a month for 25 MB with 20 connections, and it does not persist. If your cache is on Mini, be clear with yourself about whether you are migrating data or just moving where the cache lives. For a pure cache the honest answer is often that you do not need to copy anything at all: point the app at the new instance and let it warm up. Premium-0 at $15 a month is the tier where the contents are worth carrying across (both prices verified as of late August 2026 from Heroku Elements).

Connections are TLS-only on every plan, so the string always starts with rediss://, and Heroku presents a self-signed certificate on it. Our importer accepts that certificate for the copy, so there is nothing for you to configure: paste the string on the migration page, pick Valkey as the target, and the copy runs the same way as the Postgres pass. Land it on Valkey rather than Redis, because the store is Valkey 9 by default and a Valkey 9 key payload is newer than a Redis 7.2 target can read.

Swap the config var and restart

Point the app at the new database. The shape I recommend is a config var you own rather than the one the add-on manages, so that nothing about the add-on's lifecycle can fight you mid-cutover:

bash
heroku config:set PRIMARY_DATABASE_URL="postgresql://layerbase:<password>@your-host.cloud.layerbase.dev:5432/app?sslmode=require" -a your-app-name

Then read PRIMARY_DATABASE_URL in your app with a fallback to DATABASE_URL, deploy that one-line change ahead of the cutover, and the switch becomes a config change rather than a code change. Restart afterwards so every process is definitely holding the new value:

bash
heroku restart -a your-app-name

Keep sslmode=require in the string. Our endpoints are TLS.

Leave the Heroku Postgres add-on attached and running for a few days. It costs you one more billing period and buys you a rollback that takes thirty seconds instead of a restore.

Verify before you believe it

Row counts first, on both ends, on the tables that matter:

sql
SELECT relname, n_live_tup
FROM pg_stat_user_tables
ORDER BY n_live_tup DESC
LIMIT 20;

n_live_tup is an estimate, so it is a fast smell test rather than proof. On the handful of tables you actually care about, run a real SELECT count(*) on both sides and compare the numbers.

Then check the things that are easy to forget:

  • Extensions. If you use pgvector, pg_trgm, citext, or anything similar, run CREATE EXTENSION IF NOT EXISTS ... on the new database before your app touches a query that needs it.
  • Sequences. On any table you write to immediately, confirm the sequence resumes above the max id. A clean dump handles this, but a table you write to in the first minute is the worst place to discover otherwise.
  • Your test suite, pointed at the new database, before production is.

For the Valkey pass, DBSIZE on both sides is the equivalent one-line check, and spot-check a few keys with TTL to confirm expiries came across rather than being reset.

What does not come along

Worth stating plainly so nothing is a surprise on Monday:

  • Kafka, and every non-database add-on. They stay on Heroku, still attached to your app, still billed.
  • Heroku Postgres backups and their retention history. Your new database gets its own backup regime; the old snapshot list does not transfer. If you want a Heroku backup archived, download it before you detach the add-on.
  • Rollbacks, forks, and followers. Those are Heroku Postgres features, not properties of your data. On the other side, branching is the thing that fills the same slot in a workflow, and it is available on every plan including free.
  • Dyno configuration. Nothing about your app changes except the connection string it reads.

FAQ

Do I have to move my app off Heroku too?

No, and I would not do both at once. Dynos keep running on Heroku, deploys keep working, and the app just opens its connections to a different host. Splitting the two changes means that if something breaks you know which one did it.

Why can't I use a DATABASE_URL I already had?

Because the value in your app's config is the only string guaranteed to be current, and a copy in your notes may not be. Read it with heroku config:get DATABASE_URL -a your-app-name immediately before you start the import. Authentication failures during a Heroku migration are almost always a stale string.

Is Heroku Key-Value Store the same thing as Redis?

It was renamed from Heroku Redis on 11 October 2024 and it runs Valkey, with 9.0 the default, 8.1 available, and 7.2 deprecated with an end of life of 19 December 2026. Valkey is wire-compatible with Redis, so your client does not change, and we host Valkey too, which makes this a host change rather than an engine change.

Do I need to do anything about the self-signed certificate?

No. Heroku presents a self-signed certificate on the Key-Value Store's TLS endpoint, and our importer accepts it for the copy, so pasting the rediss:// string is the whole step.

How do you export data from Key-Value Store when SAVE and BGSAVE are blocked?

With SCAN, DUMP, and RESTORE, which are not blocked. A non-blocking scan walks the keyspace and each key is dumped and restored on the destination with its type and TTL. That is why there is no need for an RDB file, which Heroku does not offer for download anyway.

Will the migration take my database down?

No. The copy is read-once against your Heroku database, which keeps serving your app throughout. The only interruption is the moment you swap the config var and the dynos restart. Writes that land on Heroku after the dump begins are not in the dump, so take the copy close to the cutover and keep the write window short.

Will 20 connections be enough to run the dump?

Usually, since the dump takes one. The risk is what else is holding connections at the same time, so run the copy during a quiet period, or scale workers down for its duration. essential-0 and essential-1 allow 20 connections, essential-2 allows 40, and standard-0 allows 200.

What about my other Heroku add-ons?

They stay. Kafka, mail, logging drains, the Scheduler, and everything else remain attached to your app on Heroku, billed as before. This migration moves databases only.

Can I roll back?

Yes, if you leave the Heroku add-on attached for a few days after cutover. Rolling back is then a config var pointed at the old string. Once you detach the add-on, that door closes, so detach when you are confident, not on cutover day.

Starting

Start at the Heroku migration page with a fresh DATABASE_URL. Postgres and Valkey are both on the free tier, which is $0 for 2 databases and 5 GB with no card, so you can run the copy, point a staging app at the result, and take a week to decide before anything about your production setup changes. If you would rather start empty and load a dump yourself, create a Postgres database or a Valkey database directly.