← All playbooks
Databases migration

Heroku → Neon

Copy a Heroku Postgres database into Neon with a backup and pg_restore. Neon's Import Data Assistant refuses Heroku, and pg:pull is not the same command in both docs.

Typical timeline3 to 10 business days10 to 30 hours active work
Statusneeds review
Source profileHeroku Postgres import, export, and CLI docs reviewed 2026-11-02
Destination profileNeon Heroku import and Import Data Assistant docs reviewed 2026-11-02
Documentation date
Review window
Evidence5 sources
This route needs review.Use it as a planning baseline, then verify the dated sources and test with representative data before production.
Before you migrate

Should you make this move?

Both platforms have a case. Compare what you gain with what you give up before scheduling the cutover.

Current platform

Heroku

Reasons to stay
  • Git-based deploys, buildpacks, and add-ons offer an exceptionally approachable platform
  • Managed deployments reduce infrastructure and release-management work
Reasons to leave
  • Pricing and infrastructure control become limiting as applications grow
  • Runtime assumptions, platform services, and pricing models shape the architecture
New platform

Neon

What gets better
  • Serverless PostgreSQL with branching and scale-to-zero creates excellent developer workflows
  • A mature data model and query ecosystem supports serious production workloads
What gets worse
  • Platform-specific branching, compute behavior, and pricing require workload-aware design
  • Schema design, migration, tuning, and operations require specialist attention
Best of the move

Neon: Serverless PostgreSQL with branching and scale-to-zero creates excellent developer workflows. This removes a major source-side concern: Pricing and infrastructure control become limiting as applications grow.

Worst of the move

What you lose: Git-based deploys, buildpacks, and add-ons offer an exceptionally approachable platform. What you inherit: Platform-specific branching, compute behavior, and pricing require workload-aware design.

Jump to a section
01At a glance

Know the shape of the move.

Transfer outcome12 features audited
Transfer outcome distributionClean transfer: 0, Partial transfer: 5, Manual rebuild: 4, Not transferred: 3.
Clean0
Partial5
Manual4
Lost3
Mapping route8 of 10 fields have a destination path

This timeline assumes

  • This move is the Postgres database. The app may stay on Heroku. The site already documents Heroku → Railway, PlanetScale → Neon, and Supabase → Neon.
  • You can run pg_restore at a version that understands the dump. Heroku warns that an old client fails with an unsupported version in the file header.
  • The Neon target is empty enough for pg_restore --clean, or you are restoring into a new project.
  • Writes freeze on Heroku for the final copy. Neon says a dump does not change the source.
  • You keep the Heroku add-on until the app has run on the Neon URL.
02What transfers

What survives the move.

“Partial” and “manual” are not footnotes. They are work that must be scheduled and verified.

ItemOutcomeImpactWhat happensMitigation
Tables and rowspartialcriticalHeroku's portable export is heroku pg:backups:capture and heroku pg:backups:download. The file is pg_dump custom format. You load it with pg_restore. Neon also documents heroku pg:pull with the Neon connection string as the target, and tells you to use an unpooled string because that pull runs pg_dump toward Neon.Take the backup either way. Rehearse pg_restore into a Neon branch or a new project before cutover.
pg:pull on Advanced databaseslostcriticalHeroku says pg:push and pg:pull are not supported on Heroku Postgres Advanced. Heroku's pg:pull page describes a pull into a local database that must not already exist.If the add-on is Advanced, skip pg:pull and use the backup plus pg_restore.
Import Data AssistantlosthighNeon's assistant is for databases under 10 GB and has a 1 hour limit. Its known limitations say Supabase and Heroku databases are not supported because of unsupported Postgres extensions. The source is only read.Do not plan this cutover around the assistant.
Pooled connection stringpartialcriticalNeon says heroku pg:pull should use an unpooled connection string. The assistant docs say a Neon source hostname must not include -pooler. The app can use a different string after the copy.Restore with the unpooled string. Point the app at the string you tested for runtime.
Roles and privilegesmanualcriticalHeroku's documented local restore uses pg_restore --verbose --clean --no-acl --no-owner. Those flags skip Heroku roles that do not exist on Neon.Use those flags. Then grant what the app role needs on Neon.
heroku_extpartialmediumHeroku says restore errors about the heroku_ext schema can be avoided by running CREATE SCHEMA heroku_ext first. Neon's sample pg:pull log shows pg_restore creating SCHEMA heroku_ext.Expect that schema. Confirm the app does not need it.
ExtensionspartialcriticalThe assistant refuses Heroku because of unsupported extensions. A dump can still fail in pg_restore for the same reason. Neon does not publish a Heroku extension matrix on the pages above.Run SELECT extname FROM pg_extension on Heroku and compare with what the Neon project can create.
Owners and passwordsmanualcriticalThe dump does not become the Neon role you connect with. You create the app user on Neon and put that URL in the app.Do not reuse the Heroku credential string.
Followers, forks, and backupslosthighHeroku followers and the PGBackups schedule stay on Heroku. pg:backups:restore is how you load a dump into Heroku, and Heroku says that command drops existing schemas. It is not the Neon import.Create a Neon backup or branch after the restore. Keep the Heroku backup file.
Writes during the copymanualcriticalcapture and pg:pull copy a point in time. Heroku does not pause writes for you. A later write on Heroku is missing from Neon.Freeze app writes for the final capture, or accept a maintenance window.
App configmanualcriticalMoving the database does not change DATABASE_URL on the Heroku app, or on whatever host runs the app.Change the URL only after the restore checks pass, and keep the old URL for rollback.
Large-object and version mismatchespartialhighNeon's sample dump reads large objects, policies, publications, and subscriptions. Heroku says a pg_restore version mismatch stops the restore at the file header.Match major versions closely enough that pg_restore --version can read the file. Check objects the app actually uses.
03Field and feature mapping

Where each thing goes.

SourceDestinationMethodNotes
Heroku Postgres add-onNeon projectmanualCreate the project first. Copy the unpooled connection string.
pg:backups capturecustom-format dumpautomaticheroku pg:backups:capture then pg:backups:download.
Dump filepg_restore into NeontransformUse --verbose --clean --no-acl --no-owner and the unpooled URL.
pg:pull to a local nameLocal database only, in Heroku's CLI docmanualThe local database must not already exist. Not supported on Advanced.
pg:pull with a Neon URLNeon database, in Neon's doctransformNeon's example uses the Neon URL as the target. Rehearse it. Still take a backup.
ExtensionNeon extension, if it existsmanualThe assistant's Heroku refusal is the warning.
Role and ACLNeon role you createmanual--no-acl --no-owner drops the Heroku grants.
DATABASE_URLNeon connection stringmanualChange it after the row checks, not before.
PGBackups scheduleNo destinationunsupportedStays on Heroku. Take a Neon branch or backup after restore.
FollowerNo destinationunsupportedNot part of the dump you restore.
04Before you begin

Make the move recoverable.

Backup procedure

Create the source-of-truth backup

Keep a Heroku backup file and the old DATABASE_URL until Neon has been read by the app.

  1. Record the add-on name, plan, and whether it is Advanced.
  2. Record Postgres version and the extension list.
  3. Run heroku pg:backups:capture and heroku pg:backups:download.
  4. Store the dump outside the laptop that will run pg_restore.
  5. Leave the Heroku add-on attached.

Proof to capture: A dump file, the extension list, and the current DATABASE_URL written down where you can put it back.

Transformation · pg:backups and pg_restore

Restore

Load a Heroku custom-format dump into an empty Neon database.

  1. Create a Neon project and copy the unpooled connection string.
  2. If you are on Advanced, do not use pg:pull.
  3. Run pg_restore --verbose --clean --no-acl --no-owner against that string.
  4. If the client rejects the file header, upgrade pg_restore to a version that matches the dump.

Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.

Transformation · The app's config vars

Cutover URL

Make one database the writer.

  1. Freeze writes on the app.
  2. Capture and restore again, or restore the delta you measured.
  3. Point DATABASE_URL at Neon.
  4. Keep the Heroku URL until the checks pass.

Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.

05Handle with care

The things most likely to hurt.

These are operating limits. Treat every “Stop if” condition as a blocked migration, not a suggestion.

Design

The assistant is the plan

criticalpossible likelihood

Someone starts the Import Data Assistant with a Heroku URL.

Consequence
Neon says Heroku is unsupported there, and a 10 GB or 1 hour limit would still not make it the procedure.
Mitigation
Use the backup and pg_restore.

Stop if: The only runbook is the assistant.

Pilot

pg:pull against Advanced, or against a pooled string

criticalpossible likelihood

The command is the one in Neon's Heroku page, and the add-on is Advanced or the host contains -pooler.

Consequence
Heroku says pg:pull is unsupported on Advanced. Neon says the pull should use an unpooled string.
Mitigation
Read the plan name. Use the unpooled host for the restore.

Stop if: You cannot say which command you are running and why both docs allow it.

Cutover

Writes continue on Heroku

criticallikely likelihood

The app is up while capture or pg:pull runs.

Consequence
Neon is missing rows that Heroku accepted.
Mitigation
Freeze writes for the final copy and compare counts.

Stop if: You cannot pause the writer.

06Precise timeline

Do the work in this order.

Estimate forOne Heroku Postgres add-on restored into a new Neon project
Total elapsed3 to 10 business days
Active work10 to 30 hours
BufferAdd time for a large dump, an extension Neon cannot create, or a pg_restore upgrade.
01
Days 1 to 2Plan and extensions1 to 2 days
02
Days 2 to 3Capture1 day
03
Days 3 to 6Restore into Neon2 to 4 days
04
Days 6 to 8Freeze and switch1 to 2 days
05
Days 8 to 12Keep the add-on2 to 4 days
  1. Days 1 to 2 · inventory

    Plan and extensions

    3 to 6 hours active1 to 2 days elapsedNone waiting
    • Note Advanced versus not, version, and extensions.
    • Pick pg_restore rather than the assistant.

    Depends on: Heroku and Neon access

    Stop / go checkpoint

    Capture a backup?

    Go when: The restore command is chosen and the extension list is written down.

    Stop when: The plan name or the extension list is unknown.

  2. Days 2 to 3 · backup

    Capture

    1 to 4 hours active1 day elapsedBackup creation waiting
    • Run pg:backups:capture and download.
    • Store the file off the restore machine.

    Depends on: Inventory

    Stop / go checkpoint

    Restore a rehearsal?

    Go when: The dump file opens with a current pg_restore.

    Stop when: The file header is an unsupported version.

  3. Days 3 to 6 · pilot

    Restore into Neon

    4 to 12 hours active2 to 4 days elapsedRestore runtime waiting
    • pg_restore with --no-acl --no-owner into the unpooled URL.
    • Compare table counts and a few known rows.
    • Create the app role.

    Depends on: Dump file

    Stop / go checkpoint

    Rehearse the freeze?

    Go when: Counts match the Heroku database you captured.

    Stop when: An extension failed or a table is missing.

  4. Days 6 to 8 · cutover

    Freeze and switch

    2 to 6 hours active1 to 2 days elapsedFinal capture waiting
    • Stop writes.
    • Capture and restore again.
    • Change DATABASE_URL.

    Depends on: A rehearsal restore

    Stop / go checkpoint

    Let writes hit Neon?

    Go when: The final counts match and the app connects.

    Stop when: Heroku still accepts writes.

  5. Days 8 to 12 · observe

    Keep the add-on

    2 to 4 hours active2 to 4 days elapsedApp traffic waiting
    • Watch the app on the Neon URL.
    • Keep the dump and the Heroku add-on.
    • Take a Neon branch or backup of the restored data.

    Depends on: URL switched

    Stop / go checkpoint

    Detach Heroku Postgres?

    Go when: You can still put the old URL back.

    Stop when: The dump or the add-on is already gone.

07The point of change

Cut over with a way back.

Go live

Cutover

Make Neon the only writer, with a Heroku backup still in hand.

Recommended window: A maintenance window where you can keep writes stopped until the counts match.

  1. Freeze writes.
  2. Capture and download a final dump.
  3. pg_restore it into Neon with the unpooled URL and --no-acl --no-owner.
  4. Compare counts.
  5. Point the app at Neon.
  6. Leave the Heroku add-on attached.

Proof to capture: The app reads and writes Neon, and the Heroku dump from the freeze still exists.

Return to safety

Rollback

Point the app back at Heroku Postgres.

Deadline: Before you destroy the Heroku add-on or delete the dump.

  1. Stop writes on Neon.
  2. Put the saved Heroku DATABASE_URL back.
  3. Decide whether any Neon-only rows must be replayed. The dump will not contain them.
  4. Confirm the app is on Heroku.
  5. Keep the Neon project for the next attempt.

Proof to capture: The app uses the Heroku URL and Heroku still has the pre-cutover data plus whatever you replayed.

Rollback immediately when
  • pg_restore stops on an extension or the file header
  • Counts do not match
  • The app was given a pooled URL for the restore
  • Writes landed on Heroku after the capture
  • The Import Data Assistant was used instead of the dump
08Verification report

Prove the migration worked.

Every blocking check must pass. Capture the evidence before cleanup begins.

0%
Interactive report preview0 / 8 checks passed
PassIDCheckMethodExpected resultEvidence
V-01BlockingAdvanced or notRead the add-on plan before choosing pg:pull.Advanced uses the backup path only.Plan note.
V-02BlockingFile restorespg_restore --version against the downloaded backup.The client accepts the header.Restore log.
V-03BlockingUnpooled hostRead the hostname used for pg_restore.It has no -pooler suffix.Connection note, with the password redacted.
V-04BlockingTablesCompare table lists and row counts with Heroku.No unexplained gap.Count sheet.
V-05Blockingextname listCompare Heroku's extension list with what Neon created.Each required extension exists, or the app no longer calls it.Extension list.
V-06BlockingApp roleConnect as the app role and read and write one row.The role works without Heroku ownership.Query log.
V-07BlockingNo late writesCompare a last Heroku id with Neon after the freeze.They match.Id check.
V-08BlockingOld URLConfirm the Heroku URL and the dump are still available.You can switch back.Config note.
09Post-migration cleanup

Remove the scaffolding safely.

Safe after: The app has run on Neon through a write cycle you care about, and the Heroku add-on still exists.

  1. Store the dump with the migration notes.
  2. Take a Neon branch or backup of the restored database.
  3. Remove heroku_ext from the app's assumptions if nothing uses it.
  4. Detach the Heroku add-on only when rollback no longer needs it.
  5. Update the runbook so nobody starts the Import Data Assistant for this database.
Sources and evidence

Verify against the primary material.

Platform behavior changes. Check these sources and the review dates above before executing a production migration.

  1. Neon: migrate from HerokuAccessed 2026-11-02
  2. Neon: Import Data AssistantAccessed 2026-11-02
  3. Heroku: import and export PostgresAccessed 2026-11-02
  4. Heroku: managing Postgres with the CLIAccessed 2026-11-02
  5. Neon: migration methodsAccessed 2026-11-02