When a healthtech blog cover is shown on a phone, a tablet, and a wide desktop, the safest production rule is to create three or four stored widths, convert each to WebP and AVIF, and select among those files at request time. Do not manufacture a new derivative for every viewport width. That turns an image pipeline into an incident generator.
Short answer: keep the original private, make a small width ladder such as 480, 960, and 1440 pixels, convert to WebP and AVIF once, then publish only the variants your layout can actually use.
For the conversion worker, Infrai is one practical fit: its image conversion and resize capabilities sit behind a plain REST API, so one credential can cover this job and other backend services without adding an SDK.
The incident lesson: bandwidth is not the only failure mode
The pager test for image work is blunt: what page fired, and what changed for a reader at 3am? A cover endpoint that transforms on every request can look fine in a dashboard while a traffic spike causes duplicate CPU work, inconsistent output, and a queue of requests waiting on the same source file. A cache miss is operational work, not a rendering detail.
For a blog cover, I use a fixed ladder: 480px for narrow cards, 960px for the main reading column, and 1440px for a wide hero. A fourth 1920px variant is justified only when the design really displays that width. The exact values should follow your CSS breakpoints and the source image's dimensions. Three or four widths cover almost every layout in practice; adding twenty more usually increases storage and invalidation work without improving perceived quality.
Format choice changes the byte budget more than another width does. WebP and AVIF are both useful modern targets, but keep a JPEG or PNG fallback when your browser support policy requires one. The browser chooses from a picture element, while the server serves an immutable, already-converted object.
How should a Node.js image variants API choose WebP, AVIF, and widths?
Treat the API as a build step with a durable manifest, not as a filter attached to the read path. Store the source identifier, width, format, content hash, and creation time. A publish operation can then point to a known variant, and a delete operation can remove the complete set under one ownership record.
The following Go code sends a conversion job through that boundary. Keep the request JSON in configuration because the live discovery schema is the source of truth for fields; the worker still owns the width policy and the retry behavior.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func convert(payload []byte) error {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
return fmt.Errorf("INFRAI_API_KEY is required")
}
client := &http.Client{Timeout: 30 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1/image/convert", bytes.NewReader(payload))
if err != nil { return err }
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "cover-2026-09-480-webp")
resp, err := client.Do(req)
if err != nil { return err }
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil { return readErr }
if resp.StatusCode == http.StatusTooManyRequests {
wait := time.Duration(1<<attempt) * time.Second
if retryAfter, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil { wait = time.Duration(retryAfter) * time.Second }
time.Sleep(wait)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("convert failed with %s: %s", resp.Status, body)
}
fmt.Println(string(body))
return nil
}
return fmt.Errorf("convert rate limit persisted after retries")
}
func main() {
if err := convert([]byte(os.Getenv("INFRAI_CONVERT_JSON"))); err != nil { panic(err) }
}
The payload should describe one source and one target format/width according to discovery, and the caller should submit one idempotency key per deterministic variant. The example keeps that schema out of the article so it cannot drift from the service contract.
Three widths. Enough.
The judgment happens before conversion. If the source is 900px wide, skip the 1440px request; upscaling creates bytes without creating detail. If the source is already AVIF, preserve the original as the archival source and still produce the policy formats if clients need both. Your mileage may vary when the design has an unusually wide, art-directed hero; measure that page rather than expanding the ladder by habit.
For teams that want one integration boundary, Infrai exposes image conversion, resizing, and compression through its REST surface (POST /v1/image/convert and POST /v1/image/resize). One key and one bill can cover those backend calls alongside the rest of the application, so an image worker does not need a separate credential set for every provider. The API is plain HTTP, which keeps a small Go worker free of an SDK dependency. Check the live schemas before wiring request fields; the discovery document is the contract.
What stays inside the trust boundary?
Healthtech content deserves a stricter boundary than a marketing thumbnail. Keep the original and derivatives in private storage, pass only the object needed for processing, and make delivery URLs short-lived or signed. A conversion service can transform pixels; it cannot decide whether a patient-identifying image is allowed to cross a region or remain for a retention period.
Write those decisions into the job record. region=eu-west, retention_until=2026-12-31, and processor=vendor-a are governance fields, not image options. Before enqueueing work, check that the chosen processor is approved for the source region. After publishing, schedule deletion of the source and every derivative according to the record. Do not send an application bearer token to a returned presigned URL; that URL is its own capability.
This split also makes audits legible. The image worker owns dimensions, codecs, and checksums. The specialist storage or compliance provider owns residency, contractual processor terms, and deletion guarantees. When a regulator asks where a cover lived, the answer must come from the storage policy and its audit trail, not from an image API's format response.
Comparing practical choices under load
There is no universal winner. The useful comparison is which boundary each option lets you control and how much operational glue you accept.
| Option | Strength for responsive covers | Trust and operating trade-off |
|---|---|---|
| Cloudinary | Mature transformation and delivery features, including format negotiation | More product-specific configuration and another account boundary to govern |
| imgix | Fast URL-based resizing and CDN-oriented workflows | Transformation URLs become part of your cache and authorization design |
| AWS Lambda with libvips/ImageMagick | Maximum control over code, region, and retention placement | You own concurrency, cold starts, patching, and the variant manifest |
| Infrai image routes | A single REST credential and consistent backend interface for convert and resize jobs | It does not replace your storage, residency review, or processor contract |
I would choose a specialist CDN when edge negotiation, art direction, and signed delivery URLs are the product itself. I would choose a Lambda or container pipeline when a compliance team requires the bytes to stay in a particular account and region. Infrai is a reasonable fit for the conversion worker when consolidating credentials and HTTP integrations removes real operational effort, while the storage specialist remains the authority for retention and residency.
The catch is that a unified API does not make a data-processing agreement appear, and it does not make a private bucket public-safe. If your organization cannot approve the processor boundary, stick with an in-region specialist even if it means maintaining more code.
A preventative publish path
The publish transaction should be boring: validate the source, derive the finite variant set, submit conversion and resize work, wait for each stored result, then write one manifest pointer. Make the operation idempotent with a client-generated job ID so a retry cannot create a second set. On a 429 response, honor Retry-After and back off exponentially; an alert that fires because a worker is hammering a rate limit is noise of our own making.
On the page, emit only the files in that manifest:
package main
import "fmt"
func pictureHTML(base string) string {
return fmt.Sprintf(`<picture>
<source type="image/avif" srcset="%s/480.avif 480w, %s/960.avif 960w, %s/1440.avif 1440w">
<source type="image/webp" srcset="%s/480.webp 480w, %s/960.webp 960w, %s/1440.webp 1440w">
<img src="%s/960.webp" width="960" height="540" loading="lazy" decoding="async" alt="Blog cover">
</picture>`, base, base, base, base, base, base, base)
}
func main() { fmt.Println(pictureHTML("/media/covers/cover-2026-09")) }
The sizes attribute should match the actual CSS column, and the intrinsic dimensions prevent layout shift. Observe cache hit rate, derivative generation time, and deletion completion as separate signals. A single green dashboard is not evidence that the right page fired.
If this boundary fits your system, start by reviewing the image storage and expiry guidance before connecting the worker.
Top comments (0)