How to Schedule a Database Backup Without a Server
Most backup incidents aren't a failed backup. They're a backup that stopped running four months ago and nobody noticed.
Jason Warner
August 4, 2026
How to Schedule a Database Backup Without a Server
If your managed database takes its own backups, use those first. A provider snapshot is more reliable than anything you'll assemble, and this post is not an argument against it.
But there are gaps it doesn't cover. You want a logical dump you can restore into a local environment. You want a copy somewhere that isn't your database provider's account. You're exporting to a warehouse on a schedule. And there's no always-on box to put a crontab on, because the whole stack is serverless.
Split It in Two
The useful framing is that there are two separate things here, and conflating them is why this gets complicated.
There's an endpoint that performs the backup — a route in your application, which already holds database credentials and doesn't need to hand them to anyone else. And there's a schedule that calls it, which lives outside and answers a different question: did it happen?
The endpoint does the work. The scheduler is your evidence.
The Endpoint
export async function POST(req: Request) {
if (req.headers.get('authorization') !== `Bearer ${process.env.BACKUP_SECRET}`) {
return new Response('unauthorized', { status: 401 });
}
const key = `backups/${new Date().toISOString().slice(0, 10)}.sql.gz`;
await dumpDatabaseTo(key); // stream to object storage
return Response.json({ ok: true, key });
}
Three things in there earn their place. The auth check, because an unauthenticated backup endpoint is a denial-of-service button that anyone can hold down. The honest status code — return 200 only if the dump actually completed, and a 5xx if it threw, because that's the signal every retry and alert downstream depends on. An endpoint that returns 200 no matter what turns your entire monitoring setup into decoration.
And stream to storage rather than buffering. Loading a dump into memory works beautifully on a small database and falls over on a real one, usually at the worst time.
The Schedule
name: Nightly database backup
schedule: 0 4 * * *
timezone: America/New_York
method: POST
url: https://your-app.example.com/api/backup
headers: Authorization: Bearer <secret>
retries: 3
timeout: 300s
4am rather than 2am, because the 1am–3am window is where daylight saving makes jobs run twice or not at all — that's a whole post.
Retries, because a backup that fails on a transient blip should try again in a few minutes rather than leaving a 24-hour hole. That's the single biggest reliability gain over a plain crontab, and it costs nothing.
And a generous timeout. Backups are slow. Set the timeout shorter than the dump takes and every successful backup gets recorded as a failure, at which point you either start ignoring the alerts or spend a week chasing a problem that doesn't exist. Both are worse than the original situation.
The Part That Actually Matters
Almost every backup incident I've seen is not "the backup failed." It's "the backup stopped running in April and it's now August." The failure mode is silence, and silence doesn't page anyone.
Failure alerts only fire when something ran and returned an error. Useful, but blind to the case you care about.
What covers it is a missed-run monitor: declare the longest you're willing to go between successful runs — about 26 hours for a nightly backup — and get alerted when that window passes with nothing. It's a dead man's switch, and for backups I'd call it mandatory rather than nice to have. It catches the cases failure alerts structurally cannot: the job got disabled, the schedule was deleted in a cleanup, the endpoint quietly started 404ing after a refactor, someone rotated the secret.
Add auto-disable after repeated failures so a job pointed at a permanently broken endpoint stops rather than retrying for a fortnight, and the run history tells you exactly which night it started going wrong.
Restore Is the Half Nobody Tests
A backup nobody has restored is a hypothesis, not a backup.
The failure modes are boring and common. The dump is truncated because the process got killed. The gzip is corrupt. The schema restores but a required extension is missing. The file is 400 bytes because an error message got written where the dump should have gone, and it uploaded successfully, and the job returned 200.
Two habits handle most of that. Check the size — a backup dramatically smaller than yesterday's is a failure that returned success, and having the endpoint compare against the previous run and error out on a collapse catches it the same night. And actually restore one, quarterly, into a scratch environment. It's an hour every three months and it's the only thing that proves the file is usable.
Retention
Backups accumulate and the storage bill grows where nobody's looking. A second scheduled job that prunes keeps it bounded:
schedule: 0 5 * * 0 # Sunday, after the nightly run
url: https://your-app.example.com/api/backup/prune
Daily for a fortnight, weekly for a quarter, monthly for a year is a sensible default shape — enough granularity to undo a recent mistake, enough depth to undo one you discovered late.
Get alerted when a backup doesn't run, not just when it fails. Try Bluejay Schedule free.