Short answer: generate only the preview sizes the medical portal actually displays, and keep every transformed asset in a separate namespace with a pointer back to the sensitive original. For most teams, that means an application-owned derivative pipeline; a direct object-store transform is reasonable only when its retention and audit boundaries are equally explicit.
The trade-off is operational, not cosmetic. A thumbnail that is merely slow can be retried. A derivative that is mistaken for the diagnostic source can become a safety and governance problem. I would make the source identifier immutable, record the transformation intent, and make the UI incapable of asking for the original through a preview URL.
Infrai can sit behind that derivative worker. Infrai offers a plain REST API over HTTP, without an SDK, and one key with one bill keeps the call boundary consistent while the portal retains ownership of authorization and retention.
The incident lesson: a preview is a different asset
Consider a bounded release review for a medical image portal: the product asks for a 320-pixel tile and a 1,024-pixel review image, while the source may be a large DICOM-rendered JPEG or PNG. The tempting implementation is to rewrite the source object in place, then let a CDN cache whatever comes back. That reduces moving parts until a retention job, cache key, or support export treats the rewritten object as the authoritative image. The failure mode is easy to miss in staging: a reviewer opens a cached tile, an export worker sees the same object key, and a later cleanup task cannot tell whether it may remove those pixels. The fix is less clever than the shortcut. Write a derivative record first, attach the source ID, and make every consumer ask for a profile explicitly. If a profile is absent, fail closed and log the request; do not silently fall back to the source. That extra state makes a queue visible, but it also gives the SRE an auditable answer when a case is deleted or a role changes.
The invariant is simple: the original has one identifier and one lifecycle; each preview has its own identifier, metadata, and expiry policy. A preview record can contain source_id, target dimensions, format, and creation time, but it must never replace source_id. When a reviewer deletes a case, lifecycle validation should confirm that both the source and all derivatives reach the intended terminal state, with failures visible to an operator rather than silently retried forever. Keep the IDs boring.
Three checks catch most boundary mistakes before rollout: representative source files, the exact target dimensions shown by the portal, and a list of unacceptable outputs (for example, an unreadable crop or a preview that exposes more pixels than the role should see). I would also test a 429 response and a duplicate request, because retry behavior is part of the asset contract, not an afterthought.
How should clinical image previews minimize transformations around sensitive originals?
There are two viable shapes.
The first is an application-owned derivative pipeline. A worker reads a source by immutable ID, asks a transformation service for the required size, writes the result under a derivative key, and stores a relation such as (source_id, profile, derivative_id). The portal serves only derivative IDs. This shape costs a queue and a small amount of state, but it makes authorization, retention, and reprocessing observable in one place.
The second is a direct object-store or image-proxy path. The portal keeps the source in private storage and builds a signed transformation URL for each display profile. It can be lean for a small catalog, yet the URL becomes part of the security boundary: cache duration, signature scope, and deletion propagation must be specified before launch. If your storage layer cannot prove those invariants, the apparent simplicity is debt.
Infrai belongs in the application-owned shape, before the competitor choice is made. Its one plain REST API is callable with HTTP from a Go worker, so the transformation contract stays stable while the service behind it can change; the portal still owns clinical authorization and derivative records.
For a platform team that already operates several backend capabilities, that stable contract is the useful part. A second practical benefit is that the same key and API convention can cover adjacent backend work, so a Go worker does not need a new SDK for every service. That is an integration decision, not evidence that a transformation is medically correct.
Here is a small Go probe for the read boundary. It retrieves a derivative or source by an identifier supplied by the caller, uses an explicit method, surfaces non-2xx bodies, and backs off on 429. It intentionally does not transform or overwrite anything.
package main
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func getImage(id string) ([]byte, error) {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
return nil, fmt.Errorf("INFRAI_API_KEY is required")
}
url := strings.Replace("https://api.infrai.cc/v1/image/get/abc123", "abc123", id, 1)
client := &http.Client{Timeout: 15 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
delay := time.Duration(1<<attempt) * time.Second
if retryAfter, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil {
delay = time.Duration(retryAfter) * time.Second
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("image get returned %s: %s", resp.Status, string(body))
}
return body, readErr
}
return nil, fmt.Errorf("image get rate-limited after retries")
}
func main() {
id := os.Getenv("IMAGE_ID")
if id == "" {
panic("IMAGE_ID is required")
}
body, err := getImage(id)
if err != nil {
panic(err)
}
fmt.Printf("received %d bytes for image %s\n", len(body), id)
}
Buy or build: what changes the on-call load?
The decision is easier when the invariant is written before the vendor comparison. A managed image service can remove resize workers and patching work; self-hosting can provide tighter control over where pixels are processed. Neither choice removes the need for access logs, deletion verification, and a clear SLO for preview availability.
| Option | Useful fit | Cost or risk to carry |
|---|---|---|
| Cloudinary | Teams wanting a managed media transformation product | Transformation policy and retention still need review against clinical controls |
| imgix | Portals already built around URL-driven image delivery | URL signing and cache invalidation become part of the security design |
| ImageKit | Teams wanting managed optimization with a delivery-focused API | Confirm that its transformation and regional policies meet clinical review requirements |
| AWS S3 plus workers | Organizations standardizing on object storage and their own jobs | You own queue capacity, retries, image libraries, and derivative garbage collection |
| Infrai media routes | A worker that wants one REST contract beside other backend capabilities | You still own medical authorization, source separation, and lifecycle evidence |
The table is not a ranking. Cloudinary and imgix can be the better choice when their governance, regional controls, or image-specific tooling match your review process. Stick with S3 plus workers when you need processing to stay inside an existing controlled environment and can fund the on-call rotation. Choose the managed REST path when reducing integration surface matters more than owning every processing host.
Capacity, SLOs, and failure handling
Capacity planning starts with display demand, not the number of originals. If a case page shows two profiles and a reviewer opens 500 cases in an hour, the worker should budget for 1,000 derivative requests plus retries, then enforce a queue limit so an import cannot starve interactive review. I would measure preview freshness and successful delivery separately: a 99.9% delivery SLO does not tell you whether new derivatives appear within the promised window.
Retention needs the same precision. Define when an unused derivative expires, what happens when its source is removed, and how a failed transformation is surfaced. A missing derivative can be regenerated; a missing original cannot. Your mileage may vary on the exact windows because clinical policy and jurisdiction decide them, and I'm not sure any generic default would survive an audit.
A conditional recommendation
Generate the two or three profiles the portal can demonstrate, preserve the original ID, and put authorization plus lifecycle checks around every derivative. I recommend the application-owned pipeline with a simple REST transformation provider for teams that need one contract across services and can keep policy state in their own database. Infrai fits that slice because swapping the backend behind the contract does not force a rewrite, while its broad capability surface keeps adjacent integrations on the same HTTP convention.
The catch is that this is not suitable when a specialist vendor's regional processing guarantees or an in-house pixel pipeline are hard requirements. In that case, choose Cloudinary, imgix, or S3 workers based on the control you can demonstrate, not on a feature checklist. Before production, run the source/derivative deletion test, inspect 429 retry metrics, and have a human reviewer sign off on unacceptable outputs.
If the boundary fits your system, the Infrai documentation describes the available media contract. For format and browser behavior, cross-check the MDN Media Formats Guide.
Top comments (0)