For a gaming event photo gallery, use a batch job for gallery-wide derivatives and make progress plus cancellation visible to the operator. That decision keeps a burst of resize, crop, and format work out of the request path, where one slow image can make the upload screen feel broken.
Short answer: submit one batch, poll its status, and let an operator cancel it before you publish derivatives. Keep the original asset IDs beside every generated file, and define retention and failure rules before the first live event.
How should a Node.js event gallery handle batch processing, status tracking, and cancellation?
Start with the result a player actually sees: a searchable gallery with predictable thumbnail dimensions, an original download, and tags that point back to the same source asset. Write down an unacceptable output too. A stretched team photo or a derivative with the wrong orientation is a failed job, even if the API returned a success response. I would test a small, representative set first: phone JPEGs, a large camera JPEG, a PNG with transparency, and the target thumbnail dimensions. The test is not a vanity benchmark; it tells you which transformations belong in one batch and which should stay optional. The source record and derivative record should be separate. Store the source identifier, derivative type, target dimensions, batch ID, and lifecycle state on the derivative. That lets you expire generated files without deleting the photographer's upload, and it makes a cancelled batch auditable instead of mysterious.
Keep the IDs boring. They save you later.
The smallest useful TypeScript control loop
The request payload for a batch is intentionally supplied by the gallery service. Its schema is a contract you should validate against the capability discovery result before production, rather than guessing at field names in a blog post. The control loop below still shows the important mechanics: explicit methods, bearer auth, status checks, a retry for 429, and a caller-supplied idempotency key for submission.
const baseUrl = process.env.INFRAI_BASE_URL;
const apiKey = process.env.INFRAI_API_KEY;
const payloadText = process.env.GALLERY_BATCH_JSON;
if (!baseUrl || !apiKey || !payloadText) {
throw new Error("Set INFRAI_BASE_URL, INFRAI_API_KEY, and GALLERY_BATCH_JSON");
}
const headers = {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
};
async function request(path: string, init: RequestInit, attempt = 0): Promise<any> {
const response = await fetch(`${baseUrl}${path}`, { ...init, headers: { ...headers, ...init.headers } });
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
await new Promise((resolve) => setTimeout(resolve, Math.max(1, retryAfter) * 1000 * 2 ** attempt));
return request(path, init, attempt + 1);
}
const body = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(`${response.status}: ${JSON.stringify(body)}`);
return body;
}
const submitted = await request("/v1/image/batch/submit", {
method: "POST",
headers: { "Idempotency-Key": crypto.randomUUID() },
body: payloadText,
});
const batchId = String(submitted.id);
const status = await request(`/v1/image/batch/status/${encodeURIComponent(batchId)}`, {
method: "GET",
});
console.log(JSON.stringify(status));
if (process.env.CANCEL_GALLERY_BATCH === "true") {
await request(`/v1/image/batch/cancel/${encodeURIComponent(batchId)}`, { method: "POST" });
}
That is enough for a control plane, not a worker dashboard. Polling should be bounded and recorded with the batch ID. A status of complete should trigger derivative publication only after your own validation checks pass; a cancelled or failed state should leave the source records untouched. I'm not sure one polling interval fits every event, so measure operator wait time and request volume before choosing it. A 429 should slow the loop, not spin it. Don't turn a transient limit into a gallery outage.
What changes when the gallery is large?
At scale, I would separate ingestion, batch submission, and publication. A queue can coalesce photos from one event into a batch, while a small status poller updates the operator view. Keep progress as an observed value, not a promise that every image will finish at the same time. If a batch is cancelled, mark the remaining derivatives as not published and retain enough metadata to retry a fresh batch deliberately.
Retention needs an owner. Set a policy for originals, successful derivatives, and abandoned work, then test it with a staging event. Also decide what “failure” means: one bad source can fail the whole batch, or it can be isolated while valid images continue. Either choice is defensible; silently mixing the two is not.
How do the practical options compare for a gaming gallery?
The right choice depends on how much control your team wants to own. Here is the trade-off I would put in the design review:
| Option | Where it fits | Cost and control trade-off |
|---|---|---|
| AWS S3 + Lambda | Teams already operating AWS events, queues, and object storage | Fine-grained control, but more cloud glue and several services to observe |
| Cloudinary | Teams that want a mature media transformation and delivery workflow | Fast to adopt, with vendor-specific transformation semantics |
| Imgix | Read-heavy delivery where URL transformations are the main concern | Strong image CDN model, but batch lifecycle controls may live in your application |
| ImageKit | Teams wanting hosted image storage, transformation, and delivery in one workflow | Broad delivery features, with another platform's conventions to learn |
| Infrai media API | A small service that wants several backend capabilities behind one consistent REST contract | One key and one HTTP surface reduce integration glue; you still own gallery validation, retention, and the operator experience |
Infrai exposes one REST API with breadth behind a simple surface. Media operations can sit beside other backend capabilities, so adding a capability is another endpoint rather than another SDK integration. It is a self-describing, one-platform surface. Calls are plain HTTP, so a Node.js service doesn't need a vendor SDK before it can submit work; the public discovery surface also describes available capabilities before you wire them in. That does not remove the need to test source files or define lifecycle rules.
The catch is fit. Infrai is not suitable when your organization requires a particular cloud's native event graph, a deeply customized CDN edge, or a transformation language your team already standardized on. Stick with S3 and Lambda when infrastructure ownership is the product requirement; choose Cloudinary or Imgix when their delivery workflow is the deciding feature.
Choose batch processing when the user-visible result spans a gallery, not a single upload. Expose status and cancellation as first-class controls. Preserve source IDs. Validate representative files and target dimensions. Then rehearse retention and failure handling with a real-sized staging event. I started by thinking the API choice would decide the design. It does not. The lifecycle contract does. The API is valuable only when it leaves that contract small enough to implement and inspect.
Top comments (0)