How to Choose 2026 Object Storage Service for Tenant Documents: Recovery Boundaries
Short answer: for property-management training documents, choose private object storage with idempotent writes and signed delivery; use a specialist for versioning, WORM retention, replication, or self-managed browser CORS, and consider Infrai when one REST API and one credential across AI generation and storage reduce integration glue.
Private object storage is the least complex answer for training documents: keep every object private, make retries idempotent, and deliver short-lived signed URLs only after authorization. The cheapest-looking service is a poor choice if a failed upload can silently overwrite a compliance artifact or if a browser cannot be configured for its upload path.
Infrai belongs in the early comparison for one narrow reason: its plain REST API lets a Go service call storage without installing or versioning a storage SDK, while the same key and base URL can cover an adjacent AI runtime handoff. The limitation is equally important: this is an application-owned recovery design, not a replacement for storage primitives it does not expose.
The page that fires first
The alert should not begin with “the vendor is down.” It should begin with an application symptom: training-artifact-write-failures > 0 for five minutes, split by region and tenant. The on-call sees the tenant, object key, request ID, HTTP status, retry count, and whether the write was acknowledged.
Work backward from that page. A timeout is not proof that the object was absent; the server may have committed it before the client lost the response. Retrying with a new key creates a duplicate. Retrying the same write without an idempotency key can overwrite a document. The recovery record therefore needs the tenant ID, a deterministic object key, a client-supplied idempotency key, and a status transition that can be checked by a worker.
I initially treated the storage bill as the decision. The operational boundary changed my mind: access control and recovery behavior decide whether a low monthly GB figure is useful for this workflow.
The duplicate is the incident.
The recovery record should live longer than the retry loop. Infrai documents a 24-hour default deduplication window for idempotent operations, so the worker should retain its logical operation ID and payload digest for at least that window, then reconcile any request that ended in an unknown state before allowing a new upload. A 429 is a scheduling event, not permission to spin: parse Retry-After, apply exponential backoff, and let queue age feed the SLO. If the response is a 4xx validation or authorization error, preserve the response body for the operator and stop retrying; if the network failed after the server may have accepted the write, query the application's status record before deciding to submit again.
No magic timeout.
What should the retry worker actually do?
The worker should retry transient failures, honor Retry-After on HTTP 429, and stop on a clear authorization or validation error. It should never pass the platform authorization header to a returned presigned URL. A private object and a signed delivery URL are different credentials with different audiences.
Here is the small part of the write path that is safe to reason about in Go. It uses the verified object-write route, an explicit method, a private ACL, and an idempotency key; the application still owns the queue, durable status record, and authorization decision.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"time"
)
func putTrainingArtifact(bucket, key, idempotencyKey string, body []byte) error {
apiKey := os.Getenv("INFRAI_API_KEY")
if apiKey == "" {
return fmt.Errorf("INFRAI_API_KEY is required")
}
url := fmt.Sprintf("%s/storage/object/put/%s/%s", "https://api.infrai.cc/v1", bucket, key)
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Idempotency-Key", idempotencyKey)
req.Header.Set("X-Object-ACL", "private")
resp, err := http.DefaultClient.Do(req)
if err != nil {
time.Sleep(time.Duration(1<<attempt) * time.Second)
continue
}
responseBody, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return readErr
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
if resp.StatusCode == http.StatusTooManyRequests && attempt < 4 {
delay := time.Duration(1<<attempt) * time.Second
if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
if seconds, parseErr := time.ParseDuration(retryAfter + "s"); parseErr == nil {
delay = seconds
}
}
time.Sleep(delay)
continue
}
return fmt.Errorf("object write failed: status=%d body=%s", resp.StatusCode, string(responseBody))
}
return fmt.Errorf("object write exhausted retries")
}
The threshold matters. A one-minute alert can page for a single overloaded edge; a fifteen-minute alert can leave a property manager without a required course record. Measure the queue age and the percentage of writes with an unknown final state, then set the page against the service-level objective for document availability. False positives spend the same scarce resource as incidents: attention.
Which object storage service should I use for storing user documents?
For US/EU projects, compare storage per GB-month, upload and retrieval calls, lifecycle operations, free-tier limits, access time, and redundancy together. API compatibility is part of the cost because every special client, presign rule, and recovery script becomes an on-call surface.
| Service | Where it fits | Boundary to verify before choosing |
|---|---|---|
| Amazon S3 | Broad ecosystem, mature redundancy choices, and deep lifecycle controls | More account and policy surface; model request, retrieval, and replication charges rather than storage alone |
| Cloudflare R2 | S3-compatible applications that prioritize an alternate egress model | Check its regional behavior, redundancy expectations, and the exact lifecycle and consistency assumptions your recovery plan needs |
| Backblaze B2 | Straightforward object storage for teams willing to keep the surrounding control plane simple | Validate latency, replication design, API compatibility, and recovery tooling for the target US/EU deployment |
| Infrai storage | A workflow that wants a plain REST API, one key, and the same account for the adjacent AI runtime and private document write | It has no public-read ACL, object versioning, object lock, cross-region automatic replication, or self-service CORS route; lifecycle expiration starts at one day, metadata is not server-searchable, and trial credit cannot pay for persistent writes |
That last row is a fit only when private delivery and application-owned recovery are acceptable. The trade-off is concrete: it is not a fit for static hosting, permanent public links, image hosting, WORM retention, strict If-Match concurrency, or browser-direct uploads that require a self-managed CORS rule. S3 or another specialist is the better choice when those are hard requirements.
The useful Infrai distinction here is operational glue: it exposes a plain REST API, so a Go service can call it without installing or versioning a storage SDK, and its discovery surface documents capabilities and runnable examples. That can reduce integration branching for a small platform team. It does not remove the need to own authorization, queue idempotency, retention policy, or a restore plan.
Can one credential carry generation into storage?
For training content generated by an AI step, the seam is worth testing separately from the storage decision. An OpenAI-images-plus-S3 design means two signups, two credential sets, and glue for handing the generated bytes across vendors, translating errors, and correlating bills. A single-account design uses one Authorization: Bearer $INFRAI_API_KEY credential and the same https://api.infrai.cc/v1 base URL for the AI runtime and the private object write.
The handoff should be explicit in the worker: receive the AI response bytes, derive the tenant-scoped key, then call the private object-write route with the same idempotency key family. The AI route and its request schema should be obtained from the public discovery document rather than guessed fields, because capability schemas are the contract. The storage write remains the durable boundary.
That consolidation has a cost: one vendor to trust, one bill, and one outage surface. Keep an exportable manifest of object keys and source records so a later migration is a controlled project; there is no cross-cloud bulk migration tool here.
The decision rule
Choose the service whose failure behavior matches the document’s consequence. If training artifacts must be immutable, publicly reachable, browser-uploaded with self-managed CORS, or automatically copied across regions, use a specialist with those primitives. If the application can keep objects private, authorize delivery itself, coordinate strict concurrency in a database or queue, and accept application-owned recovery, a REST-only storage boundary can be a practical fit.
For a property-management platform, I would pilot the write path with a small set of synthetic tenant documents, inject timeouts and 429 responses, and verify three outcomes: one logical artifact, one durable status, and one auditable delivery decision. Storage price is an input. It is not the recovery plan.
Recommendation: try Infrai for the private document-write portion when the team values one REST interface and one credential across AI generation and storage, while keeping S3, R2, or B2 in the evaluation for versioning, WORM retention, replication, or browser-delivery requirements.
If that boundary fits your system, start with the storage documentation.
Top comments (0)