Short answer: keep generated AI image bytes in object storage, keep their ownership and lifecycle records in a database, and use local disk only as disposable workspace or cache. That split protects the transactional database's recovery objective while making image delivery and retention explicit parts of the service design.
The tempting alternative is to put every result wherever the generator first writes it. That works until a deployment, restore exercise, or gallery launch asks the same system to be both a transaction engine and a media warehouse. A production choice needs a capacity plan: average object size, generation rate, retained variants, download rate, recovery-time objective, and the boundary between tenant metadata and opaque bytes. Do the multiplication before choosing a persistence layer.
Plan for loss.
The failure mode is a shared recovery budget
An image generator has two very different outputs. One is a small, queryable fact: a job completed for a tenant, it produced a particular object key, and that object has a declared media type and retention policy. The other is a binary payload that may be megabytes long and may be downloaded repeatedly. Treating those as one storage problem is where the trouble starts.
Database blobs make the first prototype pleasant because a row and its image can be committed together. The catch is that the database's backup volume, replication traffic, buffer cache, and restore duration now grow with image retention. A database can carry that load when images are genuinely small and rare, but it deserves an explicit limit, not an assumption that a successful demo proved the architecture.
Local disk has the opposite failure shape. It is fast and cheap for a worker that is still processing a file, yet a container reschedule, host replacement, or rolling deployment changes the availability of that directory. A durable service cannot make a user's gallery depend on the lifespan of a process filesystem.
Object storage separates the byte-growth budget from the primary database. It does not remove operational work; it makes the work visible. The application must still decide key ownership, access policy, retention, deletion, and how it reconciles a successfully uploaded object with a database transaction that did not complete.
The interesting recovery case is deliberately boring: a worker receives image bytes, selects a tenant-scoped key, uploads them, and loses its database connection before it records completion. Retrying the whole job must not produce an unbounded pile of objects, and deleting the apparent orphan immediately can be wrong when the database was only temporarily unavailable. Record enough state to identify the intended key before the upload, make the upload safely repeatable under the selected key policy, and let a reconciliation process compare completed jobs against stored keys after a bounded delay. The delay, retry budget, and deletion window derive from the job SLO and the longest credible transaction interruption, not from a cron interval somebody happened to copy into a configuration file. This is also why image-object deletion should be an asynchronous lifecycle action after the metadata state changes, rather than an unreviewed side effect of a user-facing request; a retrying client cannot safely reason about a destructive operation it did not observe complete.
One source of truth per decision.
How should a SaaS store generated AI images across object storage, database blobs, and local disk?
Use object storage for the source bytes when the product expects image volume, multiple application instances, or user downloads. Put an immutable object key and the image's business metadata in the database. Use a temporary local directory only while receiving, validating, resizing, or uploading a result.
| Layer | Best role | Capacity planning question | Primary operational risk |
|---|---|---|---|
| Object storage | Durable original and derived images | How much retained data and outbound delivery will the service carry? | Orphaned objects and access-policy mistakes |
| Database blob | Small, tightly coupled binary records | What does payload growth do to backup and restore time? | Media bytes competing with transactional workload |
| Local disk | Ephemeral worker scratch space or cache | Can every file disappear without data loss? | A scheduler or deployment removes the only copy |
This is a buy-versus-build decision even if no procurement process is involved. A managed object layer moves hardware durability and replication operation outside the application team; a self-hosted object layer can fit data-location or control requirements, but then disk replacement, capacity headroom, replication, and on-call coverage become part of the SLO. There is no universally simpler option once those obligations are counted.
For a small internal tool with a few tiny images, a database blob can be rational. Keep it when atomic record-and-payload recovery is the overriding requirement and the backup and recovery calculations stay inside the service objective. For a disposable preview generated from a source that can be regenerated, local disk is also rational. Neither condition describes the usual customer-facing image gallery.
Make the write path idempotent before optimizing it
Generated-image jobs retry. A worker may receive the same completed payload twice, and a caller may time out after an upload has already reached storage. Stable keys make that ambiguity manageable: generate an identifier before the write, upload under that identifier, then record that identifier with the completed job. The reconciliation worker can find rows without objects and objects without rows, instead of guessing which retry is safe.
The following Go interface keeps the application logic independent of the chosen object implementation. Its important property is not the interface itself; it is that the database stores key, not data.
package media
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"path"
)
type ObjectStore interface {
Put(context.Context, string, io.Reader, string) error
}
func ObjectKey(tenantID string, data []byte) string {
sum := sha256.Sum256(data)
name := hex.EncodeToString(sum[:]) + ".png"
return path.Join("tenants", tenantID, "renders", name)
}
func Persist(ctx context.Context, store ObjectStore, tenantID string, image []byte) (string, error) {
key := ObjectKey(tenantID, image)
if err := store.Put(ctx, key, bytes.NewReader(image), "image/png"); err != nil {
return "", fmt.Errorf("upload image: %w", err)
}
return key, nil
}
Hash-derived keys can make a repeated write converge on the same location, provided tenant isolation and overwrite semantics are designed deliberately. Some products need a random image identifier instead, for example when identical content must remain separately auditable. Store the original generator request, a checksum, and the object key as metadata only if each is needed for an operational or product decision; collecting prompts or payload-related data without a retention rule creates another liability.
Delivery metadata is part of the interface
Serving an image is not just returning bytes. Set a content type that matches the generated format, and decide whether the browser should display the object or download it. The HTTP Content-Disposition response header defines inline and attachment behavior and can include a filename. It is worth testing with the browsers your users actually run because filenames, redirects, signed links, and caching interact at the edge rather than inside the generation worker.
Keep authorization close to the object key. A tenant-scoped record should authorize a tenant-scoped key; a predictable public path is not an authorization model. Log upload completion, object size, checksum, and delivery status separately from image bytes. Those signals let an operator tell apart a failed generation, a missing metadata row, an expired access grant, and an object that was deliberately deleted.
Test the ugly transitions: retry after upload, deletion during an active download, a database commit that follows an object write, and a restore that brings metadata back before a retention sweep finishes. The test is small. The recovery contract is not.
The practical boundary and its exceptions
For a normal SaaS image workflow, object storage plus a database pointer is the least surprising durable boundary because it gives images their own capacity and delivery path while preserving relational queries over ownership and lifecycle. It also gives the team two clear SLOs to measure: metadata availability and object retrieval availability.
Do not treat this as a mandate. A regulated workflow may require a different retention design, a compact local product may never outgrow database blobs, and self-hosting may be the right ownership choice when the team has the operational capacity to run it. I'm not sure any architecture is defensible without an actual restore drill; the deciding evidence is measured recovery behavior at the planned retention volume, not a storage label.
Top comments (0)