For white-label image delivery, define presets, watermarks, and output formats as a versioned policy before an e-commerce brand portal accepts an upload. When a thumbnail is missed, the page usually does not say “the derivative job was late.” It shows an empty image, a broken badge, or a full-size original squeezed into a card, while the on-call alert arrives later: queue age above the threshold, or a retry count that suddenly jumps to 12.
Short answer: represent every brand's delivery policy as a persisted sequence of controlled transformations, then validate each result before starting the next one. Decide per asset whether that sequence runs at upload time or on demand, and keep source-to-derivative lineage so retries and cleanup are boring.
That decision matters more than the vendor logo. A white-label portal may serve a luxury storefront as WebP with a corner mark, while a marketplace partner requires JPEG and a different watermark. The policy is data; the execution path is a small state machine.
The page that fires, and the signal we missed
The incident pattern is familiar. A seller uploads a 12 MB PNG. The upload returns, the catalog record is published, and a worker starts resize, watermark, and format conversion. A transient queue delay leaves the product card without its 400-pixel derivative. A retry then repeats the watermark step, producing a visibly darker mark or a duplicate object.
I've been paged for both versions of this: a missed job and a duplicate delivery. The useful question in the postmortem was not “which image service failed?” It was “which stage did we think had completed?”
Persist an asset ID and a job ID for each stage. Store the expected preset, watermark policy, output format, and parent derivative beside that ID. A stage can then be checked independently: source exists, resize has the expected dimensions, watermark points to that resize, and conversion produced the requested MIME type. Only after those checks should the next call begin.
Retries must converge.
The alert should fire on a signal that precedes customer-visible damage: time spent in a non-terminal state, not merely a missing thumbnail after publication. Set a deadline for polling and stop at terminal states. A threshold that is too low pages the team for normal image bursts; one that is too high lets broken cards reach shoppers. False positives have a cost, too.
How should brand portals choose presets, watermarks, and formats?
Start with a policy record, not a pile of URL parameters. The application should persist that record, while the worker submits the provider-specific request. Because the exact request schema is discoverable and can evolve, this runnable Go client takes a validated JSON object from INFRAI_IMAGE_REQUEST_JSON; a deployment can serialize its policy into the current documented schema without hardcoding an invented field here. Set INFRAI_BASE_URL to the API origin used by your account.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func main() {
baseURL := strings.TrimRight(os.Getenv("INFRAI_BASE_URL"), "/")
apiKey := os.Getenv("INFRAI_API_KEY")
payload := []byte(os.Getenv("INFRAI_IMAGE_REQUEST_JSON"))
idempotencyKey := os.Getenv("IMAGE_STAGE_IDEMPOTENCY_KEY")
if baseURL == "" || apiKey == "" || idempotencyKey == "" || !json.Valid(payload) {
panic("set INFRAI_BASE_URL, INFRAI_API_KEY, IMAGE_STAGE_IDEMPOTENCY_KEY, and valid INFRAI_IMAGE_REQUEST_JSON")
}
client := &http.Client{Timeout: 30 * time.Second}
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequest(http.MethodPost, baseURL+"/v1/image/transformation/create", bytes.NewReader(payload))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idempotencyKey)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Sprintf("request failed with status %d: %s", resp.StatusCode, body))
}
fmt.Println(string(body))
return
}
panic("rate limit retry budget exhausted")
}
The worker should derive the idempotency key from the source asset, policy version, and stage name. A retry with the same key must resolve to the same derivative record, never a second watermark. The JSON stays outside the client because its fields must come from the live schema; before execution, the application validates it against the selected brand policy. After the response arrives, persist the returned identifiers before acknowledging the queue item. For asynchronous work, save the last observed state and poll with backoff; once a stage is terminal, do not keep polling it. This ordering matters during a worker restart: if the process dies after the provider accepts the request but before the queue acknowledgement, the replacement worker uses the same key and attaches the same logical operation to the same policy stage instead of creating a sibling derivative with no lineage.
Upload-time processing is the safer default for a small, known set of storefront sizes. It makes publication atomic: the product is visible only after required derivatives validate. On-demand processing is a better fit when partners request many unpredictable dimensions, provided the first request can tolerate a cache miss and the request path has a bounded wait.
What changes between upload-time and on-demand delivery?
The trade-off is operational, not ideological. Upload-time work spends compute before anyone asks for the image, but it keeps latency out of a shopper request. On-demand work avoids unused derivatives, but it moves queue pressure into the read path and needs a cache stampede policy.
Pick one deliberately.
For a white-label portal, I normally mark a policy as required or lazy. Required stages run before catalog publication. Lazy stages are keyed by (asset_id, policy_version, width, format, watermark_revision) and are coalesced so ten simultaneous requests create one job. Either mode records lineage from the original to every derivative.
Keep the original immutable. If a brand changes its watermark, create a new policy revision and new derivative IDs; do not mutate files that an old order page may still reference. Cleanup can then remove descendants of an archived source without guessing which objects are safe.
Comparing delivery platforms without losing the runbook
Different services optimize different parts of this problem. The table is intentionally about operational shape, not a scorecard.
| Option | Strength for white-label delivery | Trade-off to verify |
|---|---|---|
| Cloudinary | Mature transformation presets, named assets, and delivery URLs make many brand variants familiar to frontend teams. | URL-driven policy can spread across templates; audit who changed a transformation and when. |
| imgix | Strong edge resizing and format negotiation for read-heavy, on-demand traffic. | You still need an authoritative job/lineage store if a derivative must be validated before publication. |
| Uploadcare | Upload workflows and CDN delivery are packaged together, which can shorten an initial integration. | Check how its processing states and retention rules map to your per-brand audit requirements. |
| Infrai | A plain REST surface can keep the transformation contract stable while the backend capability changes; a single API key and one bill also cover adjacent backend calls. | It is not the right choice when your team requires a vendor-specific visual editor or a deeply customized edge-cache rule language. |
Infrai's useful distinction here is contract portability: swapping the service behind a capability does not require changing the application-level policy record. The API is HTTP, so a Go worker can call it without installing a media SDK. Infrai's 295 routes across 20 modules use a single API key and one consolidated bill, which means the image worker does not add another secret lifecycle or another invoice reconciliation path to the runbook. In a real integration, use the documented image transformation and watermark capabilities, keep the returned identifiers, and treat response validation as part of your code rather than assuming a successful transport means a valid derivative.
I would stick with Cloudinary when a marketing team needs its mature asset UI, choose imgix when edge negotiation is the primary requirement, and choose Uploadcare when upload intake is the center of the product. Infrai fits a platform team that values one consistent backend contract across media and other services. Your mileage may vary; run a small policy matrix through staging before moving publication gates.
Instrumentation that closes the loop
For every stage, emit asset_id, parent_id, policy_version, brand_id, format, attempt, and state. Record request IDs and elapsed milliseconds, but keep payloads out of ordinary logs. A useful dashboard has three views: age by non-terminal state, derivatives rejected by validation reason, and descendants per source asset.
The alert-to-action path should be explicit. When queue age crosses the warning threshold, find the oldest stage, inspect its parent and policy revision, and replay with the same idempotency key. If the result validates, advance the state machine. If it does not, quarantine that derivative and leave the source available for a later retry. Never “fix” a duplicate by deleting the original; lineage tells you which object is safe to remove.
This discipline also makes format changes safer. A browser may accept WebP today and AVIF tomorrow, but a partner contract may still require JPEG. Store the requested output format in the policy and validate the actual MIME type and dimensions before publishing the URL. The format is part of the contract, not a cosmetic detail.
Top comments (0)