How Taxiway handles backups
Lloyd Owen
1 September 2026 · 7 min read
“Managed backups” usually means your data goes through the vendor. That is a reasonable thing for a vendor to build and a strange thing to accept, so Taxiway does it the other way round: the dump happens on your host and uploads directly to your bucket, and the only thing that reaches us is a line saying it worked.
The path
Everything in the top row happens inside your machine. The only thing crossing to us is the dashed line at the bottom, and it carries an object key, a byte count and a duration.
Why a separate container
The obvious way to dump a Postgres database is to run pg_dump inside the Postgres
container. It works, and it is the wrong shape.
Instead the agent starts a short-lived container for the job, on the same network, and
mounts a single directory into it at /taxiway-backup. The dump is written there as
dump.sql.gz, gzipped as it is produced.
Three reasons this is worth the extra moving part:
Your database container stays exactly as you deployed it. No extra process, no memory spike inside the container your traffic depends on, and nothing added to the image.
The tool version can match the engine. The dump container is chosen for the database family being dumped, so a Postgres 17 database gets a Postgres 17 dump tool. Dumping a newer server with an older client is a classic way to produce a backup that only fails at restore time, which is to say the worst possible time.
The credentials come from the desired state, not from the wire. The agent already knows the spec that database is running with, because it is the thing that started it. It resolves the username and password from there rather than accepting them in the backup instruction. The control plane does not need to send a database password to trigger a backup, so a backup instruction is not a credential-bearing message.
Volumes get the same treatment, with a trick
For file volumes (uploads, media, anything on disk) there is no dump tool, so the agent
archives the volume with tar from a busybox container.
Busybox specifically, rather than the service’s own image, because whether the image you
deployed happens to contain tar is not something a backup system should be betting on.
It is a couple of megabytes and pulls in seconds.
The trick is where it mounts. The volume under backup is always mounted at the same fixed
path, /taxiway-source, regardless of where that volume actually lives in the running
service. That means the resulting archive contains paths relative to that fixed root, with
no trace of the original directory layout, so it can be restored into a document root at
any path later. Archive a volume that lives at /var/www/html today and restore it into
/app/storage next year, without unpicking the tar.
If you have ever tried to restore a backup and found it full of absolute paths from a server that no longer exists, you will know why this is in here.
The upload
Once the file is on disk, the agent uploads it, using the AWS SDK’s S3 manager: multipart, 32 MB parts, three parts in flight.
The connection is from your host to your bucket. Your credentials, your endpoint, your region, your retention policy. Any S3-compatible target works: AWS, Backblaze B2, Wasabi, Hetzner Object Storage, or a MinIO or SeaweedFS instance on another machine you own.
We are not a hop in that path. We could not read your dumps if we wanted to, because they are never sent to us.
The unglamorous safety rails
Backups are one of those features that are easy to demo and hard to make trustworthy. The ones that matter here are the ones that stop a backup from taking down the thing it is protecting.
One at a time, per host. A database large enough to be worth backing up is also large enough that two simultaneous dumps will hurt. The runner takes a lock. The second job waits.
A disk floor. The dump size is unknown until the dump is taken, and you cannot estimate it usefully beforehand. So the check is not an estimate, it is a floor: if the host has less than a gibibyte free, the backup refuses to start and says so. Failing fast and legibly on a nearly-full host is much better than filling it the rest of the way and taking the applications down with it.
A timeout that agrees with the other side. A single capture is bounded at four hours. That number is not arbitrary: the control plane treats a backup row that has not reported after six hours as failed, and the agent’s own limit has to sit comfortably inside that window. Otherwise you get the worst outcome available: a backup marked failed that is in fact still running and still writing to your disk.
A label the reaper respects. The dump container carries a system role label. Without it, the reaper that removes unmanaged containers every thirty seconds would delete the backup mid-dump, because a transient job is by definition not in the desired state.
What we actually store
When the job finishes, the agent sends one message back:
{
"type": "backup_result",
"payload": {
"uid": "svc_9f1c…",
"object_key": "taxiway/prod/postgres/2026-09-01T02-00-11Z.sql.gz",
"size_bytes": 184219904,
"duration_ms": 42618,
"status": "ok"
}
}
That is what the dashboard renders. A list of backups, their sizes, how long they took, and
whether they worked. If you delete your Taxiway account tomorrow, every one of those files
is still sitting in your bucket, in a standard format, restorable with psql and tar and
no software of ours involved.
The point
The reason to run on your own server is usually some version of “I want to know where my data is”. A backup system that quietly routes every byte of your database through a third party undoes that, no matter how good the encryption story is.
So: the dump runs on your machine, the file lands on your disk, the upload goes to your bucket, and we get a receipt.
Backups are configured per service in the dashboard, and like everything else, they are free.