It's 14:05. A migration that shipped at 14:02 dropped the discount_code column from orders, and support tickets are already arriving. You need to restore a Neon database to the state it was in at 14:01, and you need to decide how in the next few minutes, because every new order lands in a table that no longer has the column.
Neon gives you three ways back, plus a fourth move for when you only need one table. They differ in how far back they reach, what they overwrite, and whether the project has to still exist. This guide shows you how to pick one, the exact commands for each, and how to check that what came back is what you expected.
Which restore path do you need?
Answer four questions first. How far back is the good state? Does the Neon project still exist? Do you need the whole branch back, or one table? And are you restoring into Neon, or somewhere else? If you want the background on how the native mechanics work, how Neon's native backup works covers history, branches and snapshots in depth.
| Path | How far back | Project must still exist | Scope | Destination |
|---|---|---|---|---|
| Instant restore | Inside the history window: 6 hours on Free, up to 7 days on Launch, up to 30 days on Scale | Yes | Whole root branch, every database on it | Same branch, in place |
Branch at a timestamp, then pg_dump -t | Inside the history window | Yes | One table, or a few | Your live branch |
| Snapshot restore | Whenever the snapshot was taken. Scheduled snapshots are kept up to 35 days, manual ones until you delete them | Yes | Whole branch | Same project |
pg_restore from a dump | As old as your dump file | No | Whole database, or selected tables | Any Neon project, region or Postgres host |
For the 14:02 migration, the history window covers you. The real choice is between rewinding the whole branch, which also discards every order written since 14:02, and pulling back just the column. Most of the time you want the second one.
The decision that costs the most
Every command in this guide is short. The expensive mistake happens before any of them: choosing to rewind the whole branch to fix one column, and losing every good order written since 14:02 along with the bad migration.
Path 1: Instant restore to a point in time
Instant restore rewinds a branch to any timestamp or LSN inside your history window, down to the millisecond. It is the fastest path, and the bluntest. Read these four rules before you use it.
- It overwrites. Neon calls it "a complete overwrite of the database timeline, not a merge". Everything written after the restore point is gone from the branch.
- It applies to every database on the branch, not just the one you're fixing. The
neon_authschema is restored with it. - Root branches only. Child branches do not support instant restore.
- Object Storage and Functions are not reverted. They are not part of the database timeline.
Neon keeps the pre-restore state in a backup branch named {branch_name}_old_{head_timestamp}. Existing connections drop briefly during the restore, but the connection string stays the same, so your app reconnects to the restored data.
From the Console
Select the branch, then open Postgres database > Backup & Restore and use Restore from history. Pick the date and time, and click Restore. In the confirmation dialog, choose Preview data before you commit: you can browse tables, run read-only SQL, or compare schemas against the selected point.
That preview is Time Travel: read-only queries against a temporary branch at the timestamp you chose. Use it to find the exact second before the migration ran, rather than guessing.
From the CLI
To restore a branch to its own history, use ^self with a timestamp. The --preserve-under-name flag is mandatory in this form, and it names the backup branch.
neon branches restore production ^self@2026-11-04T14:01:00Z \
--preserve-under-name production_before_restore
The timestamp is RFC 3339. Write it in UTC with the trailing Z, so the restore point doesn't shift by your local offset. You can also pass an LSN instead of a timestamp, for example ^self@0/1E887C8.
From the API
The same operation is a POST to the branch's restore endpoint. When you restore a branch to its own history, source_branch_id is the branch's own ID, and both a timestamp (or LSN) and preserve_under_name are required.
curl --request POST \
--url "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/restore" \
--header "Authorization: Bearer $NEON_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"source_branch_id": "'"$BRANCH_ID"'",
"source_timestamp": "2026-11-04T14:01:00Z",
"preserve_under_name": "production_before_restore"
}'
Picked the wrong timestamp? Run instant restore again with the backup branch as the source. Neon's docs describe this as the way to revert a restore, because the backup branch is not a parent of the restored one.
Clean up the backup branch later, not now
Keep the backup branch until you have verified the restore. It has no compute attached, so it adds storage cost but no compute cost. It also holds every write between the restore point and the moment you ran the restore, which is exactly where you will look if someone asks for an order placed at 14:03.
When you no longer need it, delete it from the Branches page. Two cases block deletion: a backup branch created by restoring a root branch from another branch, and a backup branch that has child branches. If you cannot delete it, Neon suggests connecting to it and dropping its databases or tables to free the storage.
Restore one table without rewinding the whole branch
For the dropped column, rewinding production to 14:01 fixes orders and throws away every write since then, in every table. The better move is to recover the old data on the side and copy back only what you lost.
Step 1: create a branch at a point before the damage. Neon's CLI accepts a timestamp as the --parent, which its docs call creating an instant restore branch. Here production is the project's default branch, which is what --parent falls back to.
neon branches create --name orders_recovery --parent 2026-11-04T14:01:00Z
neon connection-string orders_recovery
neon connection-string returns a direct connection by default. Keep it that way: Neon only adds the -pooler host when you pass --pooled, and pg_dump needs the direct one.
Step 2: stage the data you need. On the recovery branch, copy the lost column into a plain staging table. Writes to orders_recovery stay on that branch and do not touch production.
export RECOVERY_URL="postgresql://neondb_owner:<password>@ep-recovery-000000.eu-central-1.aws.neon.tech/neondb?sslmode=require"
export LIVE_URL="postgresql://neondb_owner:<password>@ep-production-000000.eu-central-1.aws.neon.tech/neondb?sslmode=require"
psql "$RECOVERY_URL" -c "CREATE TABLE orders_1401 AS SELECT id, discount_code FROM orders;"
CREATE TABLE ... AS copies rows but no indexes or constraints, so nothing collides with names that already exist on the live branch.
Step 3: dump the staging table and restore it into the live branch.
pg_dump -Fc -v -t orders_1401 -d "$RECOVERY_URL" -f orders_1401.dump
pg_restore -v -d "$LIVE_URL" orders_1401.dump
-t is a standard pg_dump flag that limits the dump to one table. It is general Postgres practice, not something Neon documents specifically.
Step 4: put the data back, then clean up.
ALTER TABLE orders ADD COLUMN discount_code text;
UPDATE orders o
SET discount_code = r.discount_code
FROM orders_1401 r
WHERE o.id = r.id;
DROP TABLE orders_1401;
Match the column type to your original schema; text here is an example. Orders created after 14:02 have no old value to copy, so decide with your team what they should hold. Then delete the recovery branch with neon branches delete orders_recovery.
If the migration dropped a whole table instead of a column, skip the staging step. Run pg_dump -t orders against the recovery branch and pg_restore the file into the live branch. The table and its indexes come back together, because none of them exist on the live branch anymore.
Not sure what the table looked like at 14:01? Ask before you branch. neon connection-string production@2026-11-04T14:01:00Z --psql opens a read-only Time Travel session at that moment, so you can check the rows you expect to recover.
Path 2: Restore from a snapshot
A snapshot captures a root branch at a point in time, and Neon's snapshot and backup schedule docs describe two ways to restore one. Pick based on whether you want to look before you switch.
| One-step restore | Multi-step restore | |
|---|---|---|
| What happens | The branch is restored to the snapshot state immediately | A new branch is created from the snapshot; your current branch is unchanged |
| Connection string | Stays the same; the branch ID changes | Stays the same once you finalize |
| Backup of the old state | Branch renamed <branch_name> (old) | Branch renamed <branch_name> (old) at finalize |
| Use it when | You trust the snapshot and need speed | You want to inspect the data first |
One-step restore
In the Console, find the snapshot and choose Restore > One-step restore. From the CLI, --finalize does the restore and the swap in one command:
neon snapshots list
neon snapshots restore snap-1234 --target-branch production --finalize
Always pass --target-branch. Neon recommends it explicitly, because without it a later restore can land on the branch renamed (old) by an earlier one.
Multi-step restore
Leave out --finalize to restore the snapshot into a new branch, inspect it, and only then swap it in.
neon snapshots restore snap-1234 --target-branch production --name production_from_snapshot
neon connection-string production_from_snapshot
# inspect the data, then finalize with the restored branch's ID
neon snapshots finalize br-restored-000000
Pass the restored branch to finalize, not the target. Finalizing moves the computes over, renames the restored branch to the original name, renames the original to <branch_name> (old), and moves any backup schedule and branch protection to the restored branch.
Two limits catch people after a snapshot restore. Instant restore does not work on a branch created from a snapshot restore, and an attempt returns restore from snapshot on target branch is still ongoing. And for up to 24 hours, child branches of the restored branch cannot use Reset from parent.
Running a risky migration today? Take a manual snapshot first: neon snapshots create --branch production --name pre-migration. Manual snapshots never expire unless you set an expiry, which makes them a cleaner rollback point than a timestamp you have to reconstruct later.
Snapshots and history share one property, which what Neon's native backup doesn't cover goes through in detail: they live inside the same Neon project as the data. If the project is gone, so are they.
Path 3: Restore a pg_dump with pg_restore
This is the only path that works when the project no longer exists, or when the destination is not the original project. Neon's pg_dump backup docs say you can restore to any Neon project, including across regions. Any other Postgres host works too. It assumes you have a dump; if you don't yet, backing up Neon with pg_dump is where to start.
Step 1: create the target project and database. Neon's documented flow is a new project with a database of the same name as the one you dumped.
neon projects create --name orders-restore --region-id aws-eu-central-1 \
--pg-version 17 --database neondb
Set --pg-version to the major version of the source project. If the target already exists, add the database with neon databases create --name neondb.
Step 2: get the unpooled connection string. In the Console, click Connect and deselect Connection pooling. Neon is explicit that pooled connections are not supported for pg_dump and pg_restore.
Step 3: run the restore.
pg_restore -v -d "postgresql://neondb_owner:<password>@ep-target-000000.eu-central-1.aws.neon.tech/neondb?sslmode=require" \
neondb-2026-11-04.dump
That is Neon's documented command. When the target is a different provider, or a Neon project whose roles differ from the source, add two standard Postgres flags:
pg_restore -v --no-owner --no-acl -d "$TARGET_URL" neondb-2026-11-04.dump
--no-owner skips the ALTER ... OWNER TO statements, so every object is owned by the role you connect as. --no-acl skips GRANT and REVOKE. Neither is Neon guidance; they are the usual fix when the source roles don't exist on the target.
Match your client to the server. Neon recommends the latest pg_dump and pg_restore, with a client version that matches your project's Postgres version. Check with pg_restore --version before you start, not after a failure.
Verify the restore
No error is not the same as the right data. Run these checks against the restored branch or database before you point traffic at it.
-- 1. Are the tables there?
\dt
-- 2. Row counts on the tables that matter
SELECT relname AS table_name, n_live_tup AS estimated_rows
FROM pg_stat_user_tables
ORDER BY n_live_tup DESC
LIMIT 20;
-- 3. Is the newest row from just before the restore point?
SELECT max(created_at) FROM orders;
-- 4. A record you know should exist
SELECT id, discount_code FROM orders WHERE id = 48213;
n_live_tup is an estimate. For a table you need an exact answer on, run SELECT count(*) instead, and compare it to the same count on the backup branch:
for b in production production_before_restore; do
echo "$b: $(psql "$(neon connection-string "$b")" -Atc 'SELECT count(*) FROM orders;')"
done
On a table that only grows, the restored branch should hold fewer rows than the backup branch: the difference is what was written after your restore point. If the counts match, either nothing was written in that gap or the restore point is later than you meant.
For schema, compare the restored branch with the backup branch. In Neon, the Schema Diff view in the restore preview shows the two side by side. Outside Neon, pg_dump --schema-only from both databases and a diff of the two files gives you the same answer.
Common restore errors on Neon
For the full catalogue of pg_restore failures, see how to restore a PostgreSQL backup. These are the ones specific to restoring on Neon.
Errors over a pooled connection
Neon's docs say pooled connections "are not supported and will cause errors" for pg_dump and pg_restore, without listing a single message. Check the host before anything else: if it contains -pooler, swap it for the direct host and run again.
pg_dump: error: aborting because of server version mismatch
Your local pg_dump is older than the project's Postgres. Install the client that matches the server's major version.
pg_restore: error: unsupported version (...) in file header
The dump was written by a newer pg_dump than the pg_restore you're running. Upgrade pg_restore to at least the version that created the file.
ERROR: role "app_owner" does not exist
The dump assigns ownership to a role the target doesn't have. Re-run with --no-owner --no-acl, or create the role on the target first.
ERROR: relation "orders" already exists
The target database isn't empty. Restore into a fresh database, or into a new branch, rather than layering a dump over existing tables.
restore from snapshot on target branch is still ongoing
You tried instant restore on a branch created from a snapshot restore. Neon doesn't support that yet. Use a snapshot or a dump for that branch instead.
What to do next
Once the data is back, do one thing today: write down which path you used and how long it took, then test the other two on a scratch branch. A restore you have rehearsed is a restore you can run at 14:05 without reading a guide.
Then look at what happens when the Neon project itself is the problem. History, snapshots and backup branches all sit inside it, so the only copy that survives a deleted project is a pg_dump stored somewhere else.
You can script those dumps yourself with the commands above and a cron job. If you would rather not own that script, SimpleBackups runs the Neon dumps for you, stores them outside your Neon account, and alerts you when a run fails, so Path 3 is there when Paths 1 and 2 are not.
Keep learning
- How to create a Neon database backup with SimpleBackups: backup user, connection string and schedule, step by step
- pg_dump and pg_restore guide with examples: every flag used above, and the ones we skipped
- PostgreSQL backup formats: custom versus plain, and which one
pg_restoreaccepts
FAQ
Can I restore a single table in Neon?
Not with instant restore, which rewinds every database on the branch. Create a branch at a timestamp before the damage, dump the table from it with pg_dump -t, and restore that file into your live branch with pg_restore. The rest of the live branch keeps its current data.
Does Neon instant restore delete my current data?
It replaces it on the branch you restore. Neon describes the operation as a complete overwrite of the database timeline, not a merge. The pre-restore state is kept in an automatically created backup branch, so you can restore from that branch again if you picked the wrong point.
Can I restore a Neon backup into a different region or provider?
Instant restore and snapshots only work inside the same Neon project. To move data to another region, another Neon project or another Postgres host, you need a pg_dump file. Restore it with pg_restore over a direct, unpooled connection.
How long does a Neon restore take?
Neon's docs say an instant restore should take a few seconds, and describe snapshot restores as instant. A pg_restore from a dump file is different, because it replays every row and rebuilds every index, so its duration grows with the size of the dump.
This article is part of The complete guide to Neon backup: instant restore, snapshots, pg_dump, and the copy that lives outside Neon.