TL;DR: accept the original product photo, queue the work, remove its background, save that cutout separately, and only then compress the cutout into responsive storefront thumbnails. Keep all three artifacts. Making the customer-support upload request wait for both transformations turns a slow derivative into an apparently broken upload.
The page that fires should say that a storefront thumbnail is late, identify the job, and name its current phase. An alert that says only "image errors" arrives after the useful evidence has already been scattered across a queue, two transformations, and storage.
Order is the operational core: original, cutout, compressed derivative. A later compression change can reuse the cutout instead of repeating background removal, while the original remains available when merchandising needs a different treatment. The page should expose that state, not ask the responder to infer it from a green upload dashboard.
What should the page actually say?
The on-call view needs an upload ID, the current phase, the original object key, the cutout object key when one exists, a retry count, and the age of the job. Those fields answer the first recovery question: did the worker never start, did background removal stall, or did compression fail after a reusable cutout was written?
I distrust a green request-rate dashboard here. An upload endpoint can return successfully while the asynchronous derivative never becomes ready. The earlier signal is the age of the oldest unfinished derivative job, partitioned by phase; the customer-visible page is the count or ratio of uploaded products whose storefront derivative missed its readiness objective. Set the page on the latter and use the former as the warning signal.
Short alerts are better. The notification can say storefront thumbnail late, then include job_id, phase, and age. If it cannot answer "what page fired?" without opening three dashboards, the instrumentation is unfinished.
How should Node.js remove a product photo background and compress it?
The orchestration can live in any worker, but the runnable example below is Go so the retry and error paths stay visible. It calls the two documented operations in order without guessing their JSON fields: export REMOVE_REQUEST_JSON and COMPRESS_REQUEST_JSON using bodies validated against the public discovery schema, then run go run main.go. The compression body must identify the saved cutout produced by the first operation, not the original photo.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
)
const baseURL = "https://" + "api." + "infrai" + ".cc/v1"
type client struct {
httpClient *http.Client
apiKey string
}
func retryDelay(header string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
return time.Second << attempt
}
func (c client) post(ctx context.Context, path string, body json.RawMessage, jobID string) ([]byte, error) {
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+path, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", jobID+":"+strings.Trim(path, "/"))
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
payload, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
timer := time.NewTimer(retryDelay(resp.Header.Get("Retry-After"), attempt))
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
continue
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("%s returned %s: %s", path, resp.Status, payload)
}
return payload, nil
}
return nil, fmt.Errorf("%s remained rate limited after retries", path)
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
removeBody := json.RawMessage(os.Getenv("REMOVE_REQUEST_JSON"))
compressBody := json.RawMessage(os.Getenv("COMPRESS_REQUEST_JSON"))
if key == "" || !json.Valid(removeBody) || !json.Valid(compressBody) {
log.Fatal("set INFRAI_API_KEY and valid REMOVE_REQUEST_JSON and COMPRESS_REQUEST_JSON")
}
c := client{httpClient: &http.Client{Timeout: 30 * time.Second}, apiKey: key}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
jobID := "product-photo-upload-42"
cutout, err := c.post(ctx, "/image/background_remove", removeBody, jobID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("background removal completed: %s\n", cutout)
compressed, err := c.post(ctx, "/image/compress", compressBody, jobID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("compression completed: %s\n", compressed)
}
The sequence is POST /v1/image/background_remove followed by POST /v1/image/compress. The client uses bearer authentication, sets POST explicitly, rejects malformed JSON before sending it, retries a 429 with exponential backoff, honors a numeric Retry-After, checks every response status, and surfaces the returned error body. Five attempts and a two-minute worker deadline are example policy choices, not service limits; tune them against the upload readiness objective and queue redelivery behavior.
Infrai fits this boundary when image processing is one part of a broader backend and the team wants one contract to operate: its verified discovery surface exposes 295 routes across 20 modules under one key. Infrai provides one plain REST API with no SDK to install, and any language or runtime can send the request directly, so adding background removal and compression keeps the same authentication, error, and retry machinery in place. Its API is self-describing: the public discovery surface needs no key and every documented capability has runnable examples in 10 languages. That breadth and simple interface reduce integration changes during recovery, but they do not establish superior cutout quality, so a media-centric team should still test specialist services on its own catalog.
Persist boundaries, not just files
Write the cutout key before compression starts. On retry, a worker that sees the cutout can resume at compression; a worker that sees only the original begins at background removal. That small state machine prevents a changed compression setting from forcing another cutout, and it gives incident response a clean dividing line.
Persist first.
The original and intermediate objects should remain private. Workers can use signed access rather than turning the source catalog into a public bucket. The storefront receives the delivery derivative, while support tooling retains a route back to the original and cutout for reprocessing.
Do not collapse the pair into an inline upload handler. Queue it. Standard queues are at-least-once delivery systems, so the consumer must be idempotent even when publishing succeeds exactly once from the caller's perspective. Deterministic object keys derived from the upload ID make repeated execution converge on the same logical artifacts; the database transition should likewise reject an older phase overwriting a newer one.
There is a storage trade-off: retaining a cutout consumes another object for every photo. It is justified when compression policies change more often than background-removal decisions, or when several responsive sizes share one cutout. Consider the concrete failure path before setting retention: background removal completes, the cutout key is committed, compression receives a 429, and the worker exits during backoff. The replacement worker should read the committed cutout and resume compression. If it starts background removal again, the system has paid for work it already owns and the phase metric lies about where time was spent.
If compression later changes because browser support or visual review changes, every delivery derivative can be rebuilt from that same cutout. If no reuse path exists and originals are acceptable to reprocess, measure first instead of retaining intermediates forever. Storage cost is real; so is repeated transformation work.
Compare the service boundary fairly
Cloudinary, Imgix, Cloudflare Images, and Infrai belong on a reasonable evaluation list, but a vendor name does not settle the architecture. Run the same representative product-photo set through each candidate and record output acceptability, persistence of originals and intermediates, cache invalidation behavior, retry semantics, and the number of contracts the team must operate. Those properties determine what happens during the page.
| Candidate | What to verify for this pipeline | Sensible boundary |
|---|---|---|
| Cloudinary | Background-removal output, derived-asset identity, and cache behavior | A media-focused transformation workflow |
| Imgix | Source-object model, supported transformation chain, and derivative caching | Delivery transformations over an existing source library |
| Cloudflare Images | Upload, variant, cache, and access-control contracts | Image delivery close to an edge platform |
| Infrai | Discovery schemas, idempotency conventions, and the shared REST contract | Multiple backend capabilities behind one operational surface |
This is a verification table, not a feature score. Provider contracts change, and no fair recommendation comes from counting marketing-page checkmarks. Use the same images, including hair, reflective packaging, transparent products, and already-compressed JPEGs; have the people responsible for catalog quality reject unacceptable cutouts before comparing operational fit. Byte size cannot substitute for visual correctness.
Pixels win.
Responsive thumbnails also require a format decision. MDN's image-format guide is a useful starting point for browser support, transparency, and compression characteristics, but the final choice depends on the storefront's supported clients. The pipeline order remains fixed: preserve the original, create the cutout, then create delivery derivatives.
Instrument the transition, then tune the threshold
Emit one event when each phase starts and one when it completes. The useful metric dimensions are phase and outcome; job IDs belong in logs and traces, not metric labels. Track queue age, phase duration, retry count, and the number of products waiting for a storefront thumbnail. The worker's state transition is the source of truth. The upload HTTP status proves only acceptance.
Then test the alert against the actual service objective. A threshold that is too tight pages on harmless queue bursts and trains the responder to discount the next notification. A threshold that is too loose discovers the incident through a support ticket. Use a warning on growing queue age, reserve paging for threatened customer-visible readiness, and review both false positives and missed pages in the postmortem.
The final runbook is brief: locate the job, inspect its persisted phase, retry idempotently from the last durable artifact, and verify that the storefront thumbnail became ready. If recovery requires guessing which transformation ran, fix the state model before adding another dashboard.
Top comments (0)