How to back up a Neon database, and keep the copy outside Neon
Neon is PostgreSQL with branching and a history you can restore from, which covers most
of what a backup is for day to day. It does not cover a project that is deleted, a history
window that has passed, or a copy you need to hand to someone outside Neon. Neon's own
documentation says to automate pg_dump to remote storage for those. This page is
that: the connection string that works, the command, where the file goes, the restore, and
the check. The last section is how SafeGrd runs it with no server of your own.
What Neon's restore covers, and what it does not
From Neon's plans page, read on 2026-10-08:
| Plan | History window | Snapshots |
|---|---|---|
| Free | Up to 6 hours, capped at 1 GB of change history | 1 manual; no scheduled backups |
| Launch | Up to 7 days, billed per GB-month of history | 100 manual, plus scheduled backups |
| Scale | Up to 30 days, billed per GB-month of history | 100 manual, plus scheduled backups |
- The default window is short. 6 hours on Free and 1 day on paid plans until you raise it, and the history is billed by the GB, so a busy database with a long window costs more than its storage.
- Everything lives in the project. History, branches and snapshots go when the project does, whoever deletes it: a person, a script with the API key, or an account that lapses.
- Neon offers no download of its history or snapshots. For a copy you can restore to another provider, hand to an auditor, or keep for
a year, take a
pg_dump.
1. The connection string
In the Neon console, the Connect panel shows a string with a
Connection pooling toggle. Turn it off for a backup, so the host does not
end in -pooler:
- The pooled endpoint breaks
pg_dump. It is PgBouncer in transaction mode, which does not supportSET, andpg_dumpstarts with one. Neon's docs say pooled connections "will cause errors" for it. - The direct endpoint has a connection limit set by the compute size, 104
on the smallest with 7 reserved. A backup is one connection, or one per
--jobsfor a parallel directory-format dump. - Autosuspend means the first connection after an idle period wakes the compute, which takes a few seconds. A backup tool with a short connect timeout should retry once.
sslmode=requireis in every string Neon shows. Keep it.
2. Make a read-only role
neondb_owner can write. Back up as a role that cannot, so a leaked backup
credential changes nothing. Run this in the SQL editor:
Neon runs PostgreSQL 14 and later, so pg_read_all_data is available and covers
tables made after the grant. A table with row-level security dumps only the rows its
policies show this role; give it BYPASSRLS if you use RLS.
3. Dump it
Neon's guide adds -v to watch it. Match the client to the project's major
version: an older pg_dump refuses a newer server. One dump covers one database
on one branch, so a project with several databases, or a branch you also want, is one command
each. --no-owner --no-privileges keeps Neon's role names out of the dump, which
is what you want when the restore target is not Neon.
4. Encrypt it, ship it, schedule it
A dump holds every row in the clear. Encrypt it on the way out and write it to a bucket in an account that is not Neon's and not the application's:
Neon has no server to put a cron job on, so Neon's own guide runs this from a scheduled
GitHub Actions workflow; the Supabase page has a 10-line
workflow that is the same for Neon with the direct string in the secret.
set -o pipefail makes a failed dump fail the job instead of uploading a
truncated file. The bucket policy, a key that cannot delete and the lock on the bucket are
in the S3 guide and
the Object Lock guide.
A dump reads the whole database every night, and Neon meters egress after the plan's
allowance (5 GB on Free, 500 GB on Launch and Scale, from the same page). safegrd estimate prints what a nightly, weekly or hourly schedule
would move for your database (estimate).
5. Restore
Into Neon: make a new project or a new database, with the same name as the one dumped, and restore over its direct string. Into anything else: create an empty database on any PostgreSQL of the same or a newer major version.
Use a direct string for the restore as well. --exit-on-error stops at the
first failure instead of carrying on and reporting success; --jobs=4 restores
four tables at once, from a file, for a large database. The dump says CREATE
EXTENSION for each extension the database used; a target that lacks one fails there,
so install it first.
6. Check what came back
Compare each table's row count in the restored database with the source, as How to test that a backup restores shows, and do it on a schedule.
How SafeGrd does it
SafeGrd takes the backup from a scheduled GitHub Actions job, or on a machine it starts for
each backup from the connection string, over the direct endpoint, and encrypts it
before upload. Each backup uploads only what changed since the last one; the read is still
the whole database, so Neon's egress is about the database's size per backup, which
safegrd estimate prints. The drill restores into a throwaway PostgreSQL of the
same major version, and the recovery document beside each snapshot lists the extensions the
database had, so a restore target can be prepared before it is needed. SafeGrd does the same job on every platform; the PostgreSQL page has what it dumps, how it encrypts, and what a drill checks.
Run your first Fire Drill The GitHub Actions setup
- uses: safegrd/backup-action@v0
with:
config: ${{ secrets.SAFEGRD_CONFIG }}
database-url: ${{ secrets.DATABASE_URL }} # the direct string, no -pooler
The setup is the Supabase guide's with Neon's direct string in DATABASE_URL.
With no workflow at all, Back up on SafeGrd under Surfaces in the console takes the
same string; a machine SafeGrd starts for each backup dumps and encrypts it, then is
destroyed, and the string is sealed and given only to that machine
(how).
Questions
Does this replace Neon's instant restore?
No. Instant restore takes the branch back to any second in its window, which a dump cannot. The dump is the copy for after the window, after the project, and for anywhere that is not Neon.
Which branch?
Whichever the connection string names. The main branch is the usual one. A dump of a child branch is a dump of that branch's data as it is, not of its parent.
Related
- How to back up a PostgreSQL database: pg_dump's formats and flags, restore and the check
- Supabase backup: the same workflow, with Supabase's pooler and schemas
- pg_dump command builder: pick the flags and get the matching restore