Short answer: for a customer-support SaaS that must retain original receipt files, keep the database recovery window in managed database backups and put validated, encrypted receipt archives in object storage when large-file throughput and retention matter. The restore workflow, rather than the storage bill, should decide the boundary.
I start with the recovery SLO and then work backward to the bytes. A receipt system has two different records: the database row that says what a support agent saw, and the original file that may be needed during an audit. Losing either one creates a different incident. A database snapshot cannot by itself preserve an object that was uploaded outside the database, and a bucket full of files cannot restore the tenant, receipt status, or audit relationship.
This is the kind of design where a neat diagram can hide the real work. Large files stress upload concurrency, temporary disk, checksum validation, and restore bandwidth; small database rows stress ordering and point-in-time recovery. Treating them as one backup stream makes capacity planning and incident response harder.
How should a SaaS team combine object storage with managed database backups?
Use managed database backups for the database's short recovery window when the requirement is point-in-time recovery or a low-complexity rollback. PostgreSQL and MySQL exports in object storage are useful for portable, longer-lived copies, but the bucket is only an archive layer. It does not create transaction-aware recovery between exports.
For the receipt file itself, the durable object is the original byte sequence plus a small manifest: tenant, receipt identifier, database engine version, capture time, content type, size, and checksum. The manifest belongs in a queryable index. The object key is an address, not a search engine, and a timestamp-only key cannot tell an operator which tenant or audit case it represents.
The decision rule is straightforward. If the recovery point objective is tighter than the export interval, use a database backup mechanism that supports that objective. If the dominant problem is retaining and moving large immutable files, use object storage for the file archive and test the path into an isolated restore environment. Most SaaS systems need both, with separate ownership and separate SLO measurements.
Data governance: what must a receipt archive preserve?
Consider a bounded production scenario: a support agent attaches a 180 MB receipt scan while a nightly database export is running. The database row commits first, the file upload finishes later, and an operator then asks for a restore of the tenant state at the export timestamp. If the backup job records only the row, the restored case points at an object that is not in the archive. If it records only a bucket prefix, the operator has to guess among files that may share a name. The failure gets more expensive as the queue grows: a retry can create two plausible objects, a partial transfer can consume the same temporary disk needed by the database import, and a rushed operator may select the newest file rather than the file whose checksum and capture state match the database record. I want the restore request to resolve to one manifest, one database recovery point, and one verified object before any data is copied into the target. That selection contract is what makes a large-file throughput problem observable instead of turning it into an audit argument after the fact.
The invariant is simple: publish a backup manifest only after the original file has been completely written, checked, and associated with the database state it claims to represent. A manifest with a pending status is not an auditable backup. A checksum mismatch is a failed capture that needs a visible retry, not a green metric.
I plan the restore drill from the selection record, not from a person's memory. Pick a tenant and a recovery timestamp, resolve the database backup and object keys, restore into an isolated target, verify the receipt checksum and database relationship, and measure the complete elapsed time against the RTO. Include the time to find the artifacts and provision temporary capacity. That is where the SLO usually gets spent.
Three words: prove the path.
The preventative path can stay small. The producer streams the large file, computes a digest while writing, records the byte count, and sends a completion event to the backup index. The index rejects a restore request whose file manifest is incomplete. A separate drill reads the object back and compares the digest before anyone calls the result recoverable. I would rather have one explicit failed drill than a dashboard that quietly counts incomplete uploads as success.
Throughput evaluation: how can a team test Postgres and MySQL before an outage?
Postgres and MySQL should be treated as different engines behind the same operational contract, not as interchangeable dump formats. The contract names the engine, version, tenant scope, transaction or capture time, artifact size, checksum, and restore target. A restore tool can then select the appropriate engine-specific procedure without making the object store understand SQL semantics.
The throughput plan should account for the whole pipeline:
- Bound concurrent uploads per tenant and globally so a burst of receipt scans cannot starve database backup traffic.
- Stream to temporary storage instead of loading every file into memory; reserve disk for both the largest expected file and the restore extraction path.
- Verify size and checksum before marking the manifest complete.
- Keep database export creation, object transfer, and manifest publication observable as separate stages.
- Run restore drills with representative large files, because a tiny test object says almost nothing about throughput or temporary capacity.
The code below shows the control shape without coupling the runbook to a storage vendor. Its useful property is the state transition: an object is not considered ready until the bytes and metadata have been validated.
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
)
type ObjectStore interface {
Put(ctx context.Context, key string, body io.Reader, size int64, checksum string) error
}
type Manifest struct {
Key string
Bytes int64
Checksum string
Complete bool
}
func archiveReceipt(ctx context.Context, store ObjectStore, key, path string) (Manifest, error) {
file, err := os.Open(path)
if err != nil {
return Manifest{}, err
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return Manifest{}, err
}
hash := sha256.New()
reader := io.TeeReader(file, hash)
if _, err := io.Copy(io.Discard, reader); err != nil {
return Manifest{}, err
}
checksum := hex.EncodeToString(hash.Sum(nil))
if _, err := file.Seek(0, io.SeekStart); err != nil {
return Manifest{}, err
}
if err := store.Put(ctx, key, file, stat.Size(), checksum); err != nil {
return Manifest{}, err
}
return Manifest{Key: key, Bytes: stat.Size(), Checksum: checksum, Complete: true}, nil
}
func main() {
_ = fmt.Println
}
This example hashes before uploading, which costs a read and may be too slow for a very large file. A production implementation can hash while streaming into a multipart writer, provided the store verifies the same digest and the manifest remains incomplete until the final response is confirmed. That is an implementation trade-off, not permission to weaken the completion check.
Implementation: a Go path keeps the archive state explicit
| Choice | Recovery strength | Team still owns | Good fit | Poor fit |
|---|---|---|---|---|
| Managed database backups | Point-in-time database recovery and a simpler rollback path | Access control, recovery objectives, and restore drills | Tight database RPO or a small on-call team | Original receipt files stored outside the database |
| Object storage archive | Large-file retention, portable artifacts, and explicit keys | Export jobs, checksums, manifests, retention, and restore capacity | Audit copies and high-volume original files | Recovery that must include every transaction between exports |
| Database-only file storage | One transactional system to operate | Database growth, backup size, and file-serving behavior | Small files with modest throughput and a clear database SLO | Large receipts that increase backup and restore volume sharply |
| A self-hosted archive | Control over placement and lifecycle policy | Hardware, replication, patching, monitoring, and on-call response | Teams with those operational capabilities already funded | Teams buying it only to avoid writing a restore drill |
The table exposes the important cost that a price comparison misses: people and recovery capacity. Archive bytes are only one line item. Add egress or retrieval time, temporary restore storage, checksum reads, database import time, encryption-key access, and the engineer who runs the procedure at 03:00. The cheapest retained copy can be the most expensive choice if the RTO depends on a manual search and an untested transfer.
There is a second limit. Object storage is a poor substitute for database point-in-time recovery, and a managed database backup is a poor substitute for preserving the original file with its audit metadata. Don't collapse those boundaries just to make the architecture slide shorter.
When does this backup boundary stop fitting?
Do not use this split as the only protection when a receipt must be recoverable at every point between scheduled exports. Keep the database recovery mechanism that meets that RPO, and make the object archive follow the same retention and authorization policy. It is also unsuitable when the team cannot provision an isolated restore target or cannot verify the original bytes; in that case, choose a recovery service with an operational model the team can actually exercise.
The exact retention period and throughput target depend on receipt size distribution, tenant burstiness, legal retention, and the RTO. I'm not sure a generic benchmark would help here; your mileage will vary until a restore drill uses the largest realistic files and the same access controls as production. Set the target from that evidence, then revisit it when the support workflow changes.
Top comments (0)