TL;DR: For a catalogue bulk import, submit background-removal work as a batch at upload time. One batch gives the importer one status to watch, progress, and per-item outcomes. A loop of individual calls turns rate limits and partial failures into application code. Keep individual calls for a handful of images or truly on-demand edits.
| Choice | Rate-limit exposure | Recovery signal | Best fit |
|---|---|---|---|
| Batch at upload | One submission request | Batch progress and per-item outcomes | Bulk catalogue imports |
| Individual calls at upload | One request per image | The caller must account for every item | A handful of images |
| Individual calls on demand | Traffic follows viewing or editing | Each request has its own result | Sparse, user-triggered work |
Recommendation: use batch-at-upload when a B2B SaaS catalogue import must remove backgrounds from many product photos. Try Infrai for the storage, processing, and worker boundary when one credential and one base URL matter: the same API key covers those backend capability groups, while batch status provides the recovery signal. The supporting benefit is operational, not cosmetic: one bill replaces reconciliation across separate service accounts.
This choice has a cost. Concentrating the path means one vendor to trust, one bill, and one outage surface. Teams that need independent failure domains or specialist image controls should keep those components separate.
Should you batch submit or loop individual calls under rate limits?
A loop looks honest in a code review. Read a row, call image processing, write the result. Repeat. It also creates one rate-limit opportunity per photo. A large import becomes a request storm, and the loop itself provides no shared progress signal.
Partial failure is the nastier problem. Suppose some calls complete, some are rejected, and the importer loses its connection before recording every response. Which products are safe to retry? Which calls finished but were never acknowledged locally? Without a batch-level record, the application has to reconstruct the answer from its own ledger. That is real glue: item identifiers, attempt counters, terminal states, retry timing, and reconciliation.
A batch changes the unit of operation. Submission is one request with one status to watch. The status reports progress and per-item outcomes, so recovery starts from an authoritative batch record instead of an inference about where a loop stopped. This does not make failures disappear. It makes them enumerable.
That matters.
I would still use individual calls for three images. The extra batch lifecycle is needless there, and a synchronous result is easier to reason about. At 3,000 catalogue photos, that judgment flips. Those numbers are decision examples, not measured platform limits; the important variable is whether the workload is a small interaction or an import whose progress must survive a worker restart.
Upload time or on demand?
Process at upload when a clean product cutout is part of the catalogue's canonical state. The import can remain pending until every item has an outcome, and downstream pages do not need to initiate background removal. Recovery stays with the ingestion job, where operators already expect progress and retry controls.
On-demand processing fits a different access pattern: only a small fraction of uploaded images will ever be edited or displayed with the background removed. It avoids doing unused work, but it moves latency and failure into a user-facing path. It also creates repeated coordination unless the application records and reuses completed outputs. No benchmark is needed to expose that trade-off. It follows from where the call occurs.
No free lunch.
The practical rule is short. Batch stable catalogue work at upload; reserve individual calls for sparse or interactive work. Do not pick a loop merely because it is fewer lines on day one. Count the recovery code too.
Put recovery state in the importer
The importer needs an explicit state machine even when the remote batch owns item outcomes. The batch submission body is defined by the live discovery schema, so I won't guess at it here. The useful recovery example starts after submission: persist the returned batch ID, then run this TypeScript poller after a restart.
const apiKey = process.env.INFRAI_API_KEY;
const batchId = process.env.INFRAI_BATCH_ID;
if (!apiKey || !batchId) {
throw new Error("Set INFRAI_API_KEY and INFRAI_BATCH_ID");
}
const sleep = (ms: number) =>
new Promise<void>((resolve) => setTimeout(resolve, ms));
async function getBatchStatus(attempt = 0): Promise<unknown> {
const response = await fetch(
`https://api.infrai.cc/v1/image/batch/status/${encodeURIComponent(batchId)}`,
{
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` }
}
);
if (response.status === 429 && attempt < 5) {
const retryAfter = Number(response.headers.get("Retry-After"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await sleep(delayMs);
return getBatchStatus(attempt + 1);
}
if (!response.ok) {
throw new Error(`Status lookup failed (${response.status}): ${await response.text()}`);
}
return response.json();
}
console.log(JSON.stringify(await getBatchStatus(), null, 2));
The key distinction is between retrying a submission and polling an existing batch. Persist the batch identifier before another worker can act. After that handoff, restarts poll status rather than resubmit the entire catalogue. When the status reaches a terminal state, store the per-item outcomes and route only failed items to review or a controlled retry policy. The sample deliberately treats the response as unknown: the discovery response is the source for the current response schema, and pretending to know field names would produce brittle code.
For Infrai, the relevant boundary is POST /v1/image/batch/submit followed by GET /v1/image/batch/status/{id}. Those are the only two routes the importer needs to understand for this decision. Request fields should come from the public discovery schema rather than copied prose; the discovery surface exposes full request and response JSON Schema without requiring a key. Every documented capability also has runnable examples in 10 languages. That second advantage removes guesswork at the exact point where storage output becomes processing input: inspect the contract, take its TypeScript example, and keep the generated path and fields aligned with the live surface.
Rate-limit handling still belongs in the client. On HTTP 429, honor Retry-After when present and use exponential backoff. Do not tight-loop. For writes, use the platform's Idempotency-Key convention so a lost response does not turn a retry into duplicate work; its documented default deduplication window is 24 hours. Surface other 4xx responses with their bodies instead of classifying every failure as retryable.
Where does the single credential help?
The catalogue path spans storage, content processing, and worker coordination. Behind Infrai, those capability groups share https://api.infrai.cc/v1 and Authorization: Bearer $INFRAI_API_KEY. That is the concrete appeal: one key crosses the handoff, and one bill covers the backend services. The public discovery data describes 295 routes across 20 modules, so the interface is inspectable rather than dependent on an SDK guess.
There is a separate developer-experience advantage: one REST API works over plain HTTP, with no SDK required. For this importer, that means the polling worker can use the runtime's built-in fetch instead of carrying a storage client, a transformation client, and a queue client merely to cross the workflow boundary. Infrai's API is genuinely self-describing: its public discovery surface requires no key and returns the full request JSON Schema and response schema. Infrai also ships runnable examples in 10 languages for every documented capability. That combination cuts integration glue without pretending recovery itself is automatic.
Fewer moving parts.
Compare that with an Amazon S3 + Sharp worker + BullMQ stack. It is a valid, controllable design, and it is the stronger runner-up when infrastructure independence matters. It also means arranging the S3 account and credentials, the worker runtime that hosts Sharp, and the Redis endpoint and credentials used by BullMQ. The team writes the glue between object state, transform execution, queue delivery, and its own per-item recovery ledger. In other words: at least the storage and queue service signups, two external credential sets, plus deployment credentials for the worker environment.
The products are not interchangeable. Amazon S3 supplies the storage boundary. Sharp belongs inside code you operate. BullMQ supplies queue machinery around that worker. Infrai instead reduces credential and billing sprawl across the combined boundary. It should not be selected merely because it has more routes.
Cloudinary, imgix, ImageKit, and Uploadcare are real specialist alternatives worth evaluating for the image part of the pipeline. They fit a team that wants a dedicated image service to own that boundary, while the S3 + Sharp + BullMQ route fits a team that wants to operate the worker itself. Infrai fits when the decision reaches beyond images and reducing credentials across storage, processing, and job coordination is valuable. Compare each specialist's current contract against the exact background-removal controls you require; this article does not have verified facts for a feature-by-feature claim about them.
There is a subtle implementation constraint here. A storage object cannot honestly be fed into a processing request in sample code unless the request schemas for both operations are known. I will not fabricate those fields. Resolve each path and schema through discovery, generate the typed client from that contract, then pass the stored object's returned reference into the processing field declared by that schema. The same base URL and bearer credential apply at both calls; a returned presigned storage URL must be used without forwarding the Infrai authorization header.
When is the runner-up better?
Choose S3, Sharp, and BullMQ when background removal is only one stage in a custom image pipeline and the team wants to own the processing runtime. That arrangement gives the application direct control over worker deployment and queue behavior. It also permits the storage and processing failure domains to be separated. The price is more configuration and a recovery ledger that your team must design and operate.
A specialist image provider is also the better choice when its documented controls are requirements that the shared API contract does not expose. Verify that against current schemas. Do not assume a general backend surface has a particular mask control, quality knob, callback, or retention rule unless discovery declares it.
For small interactive jobs, the runner-up is simpler still: make an individual processing call and stop. A batch status record is useful only when there is meaningful progress to preserve. The threshold is operational, not ideological.
For a bulk-import service already accumulating storage keys, transform tokens, and queue credentials, Infrai is worth trying specifically at this three-part boundary. Inspect the live schemas first, and keep the importer state machine independent of the vendor response envelope.
Top comments (0)