A backup job should be judged by a measured restore drill, because a successful object upload says nothing about whether the application can recover within its recovery objectives. Start with an isolated restore, a bounded verification query set, and an alert on the age of the latest verified archive; scheduling the dump is the easy part.

Short answer: run logical exports in a separate worker, compress and store each run under a unique key, then promote it to a recovery point only after an isolated restore and application checks succeed.

The important unit of work is the recovery point, not the file. A file can be complete, readable, and still be useless because it came from the wrong database, was overwritten by a later run, or depends on roles, extensions, migrations, and capacity that were absent from the test.

What should a Postgres backup, object storage, gzip, restore, and signed URL runbook verify?

Write down the recovery point objective and recovery time objective before choosing a schedule. The service-level indicator worth watching is the age of the newest archive that has passed the whole recovery path. An upload-success metric is supporting telemetry. It is not proof of recoverability.

A useful manifest records the object key, UTC creation time, compressed byte count, digest, archive format, tool version, and run identifier. Validate the manifest schema before allocating a restore database. Use an immutable, timestamped key where the storage implementation supports it, rather than a mutable name such as latest.dump.gz.

Keep it boring.

A presigned URL is a time-limited bearer credential for a selected object. Generate it only after selecting the exact verified key, choose the shortest practical expiry, log the requestor and object, and keep it out of durable tickets and chat transcripts. AWS documents that presigned URLs are limited by the permissions of the credential that created them and by their expiration; compatible object APIs can differ in their signing details, lifecycle behavior, versioning, and retention controls. Test those details against the chosen service before an incident.

Safe worker implementation

Do not launch pg_dump from an HTTP handler. A scheduled worker needs its own workload identity, deadline, CPU and memory limits, database credential, and write permission limited to the backup prefix. That separation prevents web deploys and customer traffic from deciding whether recovery data exists.

The example deliberately depends on a small generic writer interface. It uses PostgreSQL's custom archive format, wraps the output in gzip for transport, and leaves the object-storage client as an adapter so the recovery procedure is not coupled to one provider. Restore this artifact by decompressing it before passing it to pg_restore.

package backup import ( "compress/gzip" "context" "fmt" "io" "os/exec" "time" ) type ObjectWriter interface { Put(context.Context, string, string, io.Reader) error } func Export(ctx context.Context, store ObjectWriter, databaseURL, bucket, runID string) (string, error) { key := fmt.Sprintf("postgres/%s-%s.dump.gz", time.Now().UTC().Format("20060102T150405Z"), runID) cmd := exec.CommandContext(ctx, "pg_dump", "--format=custom", "--dbname="+databaseURL) stdout, err := cmd.StdoutPipe() if err != nil { return "", fmt.Errorf("open dump output: %w", err) } if err := cmd.Start(); err != nil { return "", fmt.Errorf("start dump: %w", err) } reader, writer := io.Pipe() go func() { zipped := gzip.NewWriter(writer) _, copyErr := io.Copy(zipped, stdout) closeErr := zipped.Close() if copyErr != nil { _ = writer.CloseWithError(copyErr) return } _ = writer.CloseWithError(closeErr) }() uploadErr := store.Put(ctx, bucket, key, reader) dumpErr := cmd.Wait() if dumpErr != nil { return "", fmt.Errorf("pg_dump: %w", dumpErr) } if uploadErr != nil { return "", fmt.Errorf("upload %s: %w", key, uploadErr) } return key, nil } 

The worker must treat both the child-process exit status and upload result as required outcomes. A context deadline should be longer than the measured high-percentile export duration but shorter than the backup window. On a retry, create a new run identifier and retain enough telemetry to explain the attempt; retrying into a shared key makes a later investigation needlessly ambiguous.

Verification and rollback before promotion

A drill starts from the precise key and manifest emitted by a completed run, not from a console listing sorted by a timestamp. The recovery identity downloads the archive into an isolated environment, verifies its recorded size and digest, decompresses it, and invokes pg_restore against an empty target. Then run migration-aware checks through the application's normal data-access path.

The checks should catch a structurally valid archive that is operationally wrong: the expected schema version, a bounded tenant-count range, and a known historical record are often more useful than a health endpoint. Avoid a full scan of every large table unless the capacity plan explicitly allows it. The production database, restore target, temporary disk, network bandwidth, and object retention all need projected-volume estimates, including room for a failed cleanup cycle.

Checkpoint Evidence to retain Stop condition
Export Run ID, start and finish time, exit status Export does not complete
Upload Key, byte count, digest Stored bytes do not match the manifest
Restore Archive and restore command status Target cannot be restored cleanly
Application check Schema and domain-check results Expected data is absent or inconsistent

If a checkpoint fails, do not promote the target and do not touch production. Preserve the isolated target and logs, select the previous verified recovery point, and record the resulting recovery-point gap. If migrations cannot run backward, restore separately and follow the application's documented forward migration sequence. A signed URL does not change that rollback rule.

Choosing the recovery architecture

Logical exports to object storage are appropriate when their measured export and restore times remain inside the objectives and portable logical artifacts are valuable. The catch is that the platform team owns job isolation, secret rotation, manifest integrity, retention policy, and the rehearsal cadence. A managed database recovery facility can transfer some operational machinery, while leaving application acceptance, access policy, and drills with the team. Physical backup plus write-ahead-log recovery is a better fit when the required recovery point is tighter than periodic logical exports can provide, but it demands PostgreSQL operational depth.

Approach Good fit Team still owns Not suitable when
Logical archive Moderate datasets with portable recovery needs Scheduling, retention, restore drills Restore misses the RTO
Managed recovery Teams reducing backup-operation load Policy and application validation Export portability is required
Physical recovery Tight recovery-point targets WAL continuity and rehearsals Operators lack database depth
Self-hosted storage Existing storage operations Capacity, replication, upgrades It adds an unplanned on-call burden

Reopen the design review when restore duration trends toward the objective at projected data volume. A backup plan that works on last month's dataset but requires unreserved temporary storage or database capacity during an incident is a plan with an unpriced failure mode.

References