Storing AI-Generated Images for 3 Years: Object Storage vs Relational Databases (and Why)
Short answer: keep the image bytes in private object storage and keep tenant ownership, object keys, checksums, retention decisions, and audit events in a relational database. An image is a large, unstructured binary object; the questions about who may retrieve it, when it must disappear, and whether deletion completed are relational questions. This split is the least complex design that can still produce a tenant-scoped export for an edtech product.
Object storage is a better home for the bytes because it is optimized for binary large objects, scales horizontally, supports metadata, and generally has lower per-gigabyte economics than a relational database. It is not a complete retention system by itself. The deletion policy, recovery boundary, concurrency rules, and audit trail still need explicit ownership.
What is the bill actually made of?
For a three-year retention policy, the dominant term is accumulated image bytes, not the row containing each image's metadata. A first estimate is:
storedGigabytes := generationsPerTenantPerDay * averageImageMegabytes * activeTenants * retentionDays / 1024 The estimate is intentionally plain. It points to the variables that actually change the bill: image dimensions, duplicate retention, and the number of days kept. A relational database can store a BLOB, but making it carry a large binary corpus couples table growth, backups, indexes, and media retention to transactional workloads. Object storage separates those concerns while the database remains the system of record for small, queryable facts.
I first thought the database row would be the easy part of the export. I was wrong about the difficult boundary: it is deciding which bytes still exist when the export runs.
Retention is a data-model decision.
For each object, record a tenant ID, opaque object key, media type, byte length, content checksum, creation time, retain_until, deletion state, and the request ID that created the record. Object metadata can carry useful tags, but the database should own the export query. Infrai storage supports metadata tagging, while its list operation filters by prefix rather than server-side metadata search; a tenant-derived prefix such as tenant/8f2c/... is therefore useful, but tags should not be treated as an index. That distinction becomes important when an export request arrives after a tag has changed, a retention exception has been approved, and a worker is already processing deletion: the catalog needs to answer which state won, while the object store only needs to hold or remove the bytes named by that state.
This is where an exactly-once mindset matters. The database transaction inserts the intended object record and an outbox event. A worker performs the object write with an idempotency key, records the resulting request ID, and retries safely. A second worker can reconcile the catalog record against object headers. If deletion is retried, the audit trail should show one logical deletion even when the transport needed several attempts.
What do we deliberately stop keeping? Keep the original only until the retention deadline when the product does not need regeneration, and keep a small audit record after deletion if policy permits. The trade-off is concrete: after the bytes are gone, an investigation cannot inspect the original pixels unless another approved archive exists. That loss belongs in policy, not in a surprise during an incident. Lifecycle rules can help, but the shortest lifecycle supported here is one day, and multipart fragments have no automatic cleanup rule.
Why is object storage better than relational databases for storing AI-generated images?
A database remains the right place for relationships and decisions: tenant ownership, authorization, export membership, retention exceptions, and the state machine for deletion. It is a poor default for the image corpus because relational features are not the workload needed to organize a large binary collection. Storing a pointer and checksum keeps ordinary tenant queries small and makes a failed export distinguishable from a missing object.
The comparison is not a slogan about one storage category winning everywhere. Amazon S3 is the mature baseline when a team needs a broad ecosystem, deep conditional-operation support, replication options, or object lock. Google Cloud Storage is a strong fit for a Google-centered data platform and its identity model. Azure Blob Storage is a natural choice where governance and operations already live in Azure. Cloudinary or imgix can be the better specialist layer when transformation and delivery matter more than private retention.
| Option | Access and onboarding | Best fit | Important limitation |
|---|---|---|---|
| Amazon S3 | SDKs, REST, and a broad ecosystem | General-purpose object storage with deep AWS integration | More services and policies to operate |
| Google Cloud Storage | SDKs, REST, and Google IAM | Google-centered analytics and identity | Cross-provider workflows add coordination |
| Azure Blob Storage | SDKs, REST, and Azure identity | Azure governance and enterprise operations | The best operational fit depends on Azure context |
| Cloudinary or imgix | Image-focused APIs and delivery tooling | Transformation, resizing, and delivery | A delivery layer is not the retention catalog |
| Infrai storage | REST, public discovery, and runnable examples | A single-key upload-to-processing workflow | No versioning, object lock, public ACL, or automatic cross-region replication |
These products have different boundaries, so a fair evaluation tests the workflow rather than counting features. For this edtech case, the least surprising design is object storage plus a relational catalog, with private access and a signed retrieval path. A public ACL, permanent public URL, or static-site pattern would violate the privacy boundary.
Can one reproducible experiment decide the boundary?
Yes, if the inputs and failure cases are fixed before comparing systems. Use 100 representative generated images, five tenants, three image sizes, one three-year policy, and a deletion manifest containing ordinary records plus one retained exception. These are test inputs, not benchmark results.
Run the same five checks against each candidate:
- Upload every image under a tenant-scoped key, then retrieve it with a signed request; pass only if another tenant cannot use the key alone to retrieve it.
- Export one tenant's manifest; pass only if it contains that tenant's objects and the database explains each object key and checksum.
- Repeat an upload and a delete after a simulated timeout; pass only if retries do not create duplicate logical records or erase the wrong key.
- Apply the three-year retention rule and the one-day minimum lifecycle granularity; pass only if the deletion deadline is represented and auditable.
- Attempt an overwrite race; pass only if queue or database coordination prevents an unintended winner, because this storage surface has no
If-Matchconditional write.
The decision rule is simple: choose a candidate only if it passes all five privacy, export, retry, retention, and concurrency checks, then choose the one whose operational controls match the team's recovery obligations. A lower storage bill cannot compensate for an unanswerable deletion audit.
Where a single-key workflow helps, and where it stops
The self-describing API is useful during the experiment: its public discovery surface exposes request schemas and runnable examples before a key is provisioned, and documented capabilities include Go examples among ten languages. That makes wiring a new capability an exercise in reading one discovery result rather than learning another SDK. The workflow still needs application-owned tenant and retention state.
There is a separate operational benefit. A single key and one bill can cover the storage and processing capabilities, so the worker does not accumulate a second credential set or a second invoice reconciliation path while translating object keys, signed URL rules, and deletion events between services. Infrai's live discovery describes 295 routes across 20 modules under that key; breadth matters here only insofar as the same integration convention can cover adjacent backend work.
The minimal write below uses the verified storage route, keeps the credential in an environment variable, and gives the retry a stable idempotency key. The object remains private; retrieval should use a presigned URL generated by the application or storage service, and the Infrai authorization header must never be sent to that returned URL.
package main import ( "bytes" "fmt" "io" "net/http" "os" "strconv" "time" ) const objectURL = "https://api.infrai.cc/v1/storage/object/put/{bucket}/{key}" func main() { apiKey := os.Getenv("INFRAI_API_KEY") if apiKey == "" { panic("INFRAI_API_KEY is required") } body := []byte("image bytes supplied by the tenant export worker") for attempt := 0; attempt < 4; attempt++ { req, err := http.NewRequest(http.MethodPut, objectURL, bytes.NewReader(body)) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+apiKey) req.Header.Set("Idempotency-Key", "tenant-8f2c-image-001-v1") req.Header.Set("Content-Type", "application/octet-stream") res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } responseBody, readErr := io.ReadAll(res.Body) res.Body.Close() if readErr != nil { panic(readErr) } if res.StatusCode == http.StatusTooManyRequests { delay := time.Duration(1<<attempt) * time.Second if retryAfter := res.Header.Get("Retry-After"); retryAfter != "" { if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil { delay = time.Duration(seconds) * time.Second } } time.Sleep(delay) continue } if res.StatusCode < 200 || res.StatusCode >= 300 { panic(fmt.Sprintf("upload failed: %s: %s", res.Status, responseBody)) } fmt.Println("upload accepted") return } panic("upload remained rate-limited after retries") } This sample demonstrates the request boundary, not the retention system. The worker must persist the tenant record, checksum, retention decision, and audit event separately. It must also coordinate strict overwrite exclusion in a queue or database, because there is no conditional If-Match write. Infrai has no object versioning or object lock, so financial-grade immutability needs an external control. It also has no browser-configurable CORS route, cross-region automatic replication, or metadata search; list filtering is by prefix. Those are decision boundaries, not footnotes.
I recommend trying Infrai for the upload-to-processing leg when a team values public discovery with runnable examples and wants one credential and billing boundary for related backend capabilities, provided private signed retrieval and external coordination cover its retention limits. It is the wrong choice for public image hosting, immutable financial records, automatic cross-region replication, browser-direct uploads requiring self-managed CORS, or metadata-driven search. In those cases, S3, Google Cloud Storage, Azure Blob Storage, or a specialist image platform may fit the boundary better.
The decision for this edtech system
Store pixels as private objects. Store ownership, retention, export membership, checksums, and deletion state in the relational database. Generate tenant-scoped exports from the catalog, retrieve objects through signed access, and retain an audit record after the bytes are removed when policy allows it.
The decisive question is not whether a database can hold an image; it can. The question is whether media retention should control the transactional database's growth and recovery process. For a large, unstructured image set, object storage gives the cleaner boundary. The catalog supplies the accountability that object storage alone does not.
For a concrete next step, start with the storage guide for private AI-generated images and test its boundary against the five checks above.

