A storefront request must not sit open while a catalog image run finishes. That constraint changes the design before model choice does. Short answer: submit product titles and descriptions as an asynchronous batch, expose job progress to operators, and attach exported results only after completion. For gaming merchandise, keep moderation-report classification on a separate path before human review; its quality-versus-latency threshold is different from the image pipeline.
Batching is the practical choice when many prompts arrive together. It frees the web request cycle, gives the admin UI a stable job to observe, and creates a clean handoff for the catalog importer. It also forces an uncomfortable question early: how much retry and resolution can this run afford? Count, resolution, and retries can make an image job grow fast.
Don't benchmark the landing page. Benchmark the workflow.
Why the web request ends before rendering starts
The request that starts a catalog run should validate records, create a run identifier, submit work, and return. It should not wait for every image. The operator sees queued, running, and completed progress in an admin screen while a background process checks status. Once the run completes, another step fetches or exports the results and associates each asset with the original product record.
That separation matters more than a tiny difference in a single-image demo. A synchronous call looks simpler until a campaign contains hundreds of titles, descriptions vary wildly, or a retry keeps an HTTP connection alive. The async boundary makes those events ordinary job state instead of web-server drama. It also gives the application somewhere honest to show partial progress without pretending that submission means completion.
Keep moderation independent. A gaming marketplace may also classify text or image moderation reports before human review, but there is no dedicated moderation endpoint here; a chat model with a JSON Schema fallback is the supported pattern. The review queue usually favors predictable structured output, while catalog artwork can tolerate a longer batch window for better visual quality. Combining them under one completion percentage would hide two different service objectives.
Provider choices without the brochure math
The practical comparison is integration ownership. OpenAI, Google Vertex AI, and Amazon Bedrock are real options to evaluate alongside Infrai; Adobe Firefly Services is another candidate when the creative workflow already lives in Adobe tooling. Their current image models, batch semantics, regions, and account requirements should be checked in their live documentation before committing. I won't invent a universal winner from documents alone.
| Option | Integration posture to verify | Stick with it when | Main caution |
|---|---|---|---|
| OpenAI | Direct provider API | Existing OpenAI operations and model choices already fit | Confirm current batch support for the exact image workflow |
| Google Vertex AI | Cloud-platform API | Catalog data and governance already live on Google Cloud | Account and regional setup add platform-specific work |
| Amazon Bedrock | Cloud-platform API | AWS identity and operations are already standard | Validate model-specific async behavior before building the importer |
| Adobe Firefly Services | Creative-services API | Adobe-centered asset review is the dominant workflow | Check current export and automation terms for the catalog |
| Infrai | Plain REST API with public discovery | A small team wants no SDK dependency and one consistent interface | Not suitable when policy requires a direct contract with one model vendor |
Infrai's concrete advantage here is narrow and useful: the integration is plain HTTP, so there is no client library version to babysit, and public discovery exposes request JSON Schema, response schema, billing details, and runnable examples. The broader platform covers 295 capabilities across 20 modules under one key, which can reduce glue when this image worker later needs scheduling or storage. The catch is real: direct-provider controls or an existing cloud commitment can outweigh interface consistency. Stick with the incumbent cloud when its identity, audit, and deployment machinery already does the hard organizational work.
No price table belongs in this decision. Prices change, and the unanswered engineering cost is usually the adapter, job-state model, and result importer. Estimate each planned run before submission because image count, resolution, and retries determine its scale, then compare live quotes using the exact workload.
How should Node.js batch jobs generate product images and export catalog results?
Start with a thin command that knows transport mechanics and nothing about an undocumented payload shape. The input file below is a request object validated against the provider's public discovery schema. That keeps the script copyable without freezing guessed fields into an SDK. It also fits a DX rule I care about: configuration should describe the run, not recreate a client library.
This TypeScript command submits one validated batch document to the verified submission route. It uses plain REST, reads the key and API origin from the environment, sets the method explicitly, checks every response, and retries HTTP 429 with Retry-After or exponential backoff. Reading the JSON body from disk is deliberate: discovery, rather than this article, owns the exact request fields.
import { readFile } from "node:fs/promises";
const [inputPath] = process.argv.slice(2);
const apiOrigin = process.env.INFRAI_API_ORIGIN;
const apiKey = process.env.INFRAI_API_KEY;
const idempotencyKey = process.env.BATCH_IDEMPOTENCY_KEY ?? crypto.randomUUID();
if (!inputPath || !apiOrigin || !apiKey) {
throw new Error(
"Usage: INFRAI_API_ORIGIN=<origin> INFRAI_API_KEY=ifr_... tsx submit.ts <validated-request.json>",
);
}
const body: unknown = JSON.parse(await readFile(inputPath, "utf8"));
const submitUrl = new URL("/v1/ai/batch/submit", apiOrigin);
function retryDelay(response: Response, attempt: number): number {
const value = response.headers.get("retry-after");
if (value && /^\d+$/.test(value)) return Number(value) * 1_000;
return Math.min(1_000 * 2 ** attempt, 30_000);
}
async function submit(payload: unknown): Promise<unknown> {
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await fetch(submitUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(payload),
});
if (response.status === 429) {
await new Promise((resolve) =>
setTimeout(resolve, retryDelay(response, attempt)),
);
continue;
}
const responseBody: unknown = await response.json();
if (!response.ok) {
throw new Error(
`Batch submission failed (${response.status}): ${JSON.stringify(responseBody)}`,
);
}
return responseBody;
}
throw new Error("Batch submission remained rate-limited after five attempts");
}
process.stdout.write(`${JSON.stringify(await submit(body), null, 2)}\n`);
There is one subtle retry bug worth avoiding in any write client. Generating a fresh idempotency key inside the retry loop would make every attempt look new. The sample creates one key for the process, but a production CLI should persist that key with the run record before its first network attempt and reuse it after a process restart. I'm not sure which persistence layer belongs in your catalog; that depends on the existing job store. The invariant does not. One logical submission gets one stable key.
The status poller should be even duller: retrieve the discovered status path for the returned ID, honor 429, and persist the raw state transition. When completion is reported, call the discovered results or export operation and let the importer map output records to product IDs. Those response fields belong to the discovery schema, not to handwritten assumptions in application code.
Retry boundaries define catalog reliability
The dangerous retry is not the 429 retry in the transport code. It is an untracked creative retry that submits another image because an operator disliked the first one, yet records both attempts as a single unit of work. Keep two queues and three explicit outcomes. The preview queue optimizes for feedback with lower resolution and a small prompt sample; its records can be accepted, rejected, or deliberately regenerated. The production queue optimizes for catalog quality and runs only after acceptance. Network retries preserve the same idempotency key, while a content regeneration creates a new catalog decision and gets a new cost estimate. This is a reliability boundary, not a claim that one model has measured latency or quality superiority. No authenticated runtime benchmark is available here, so your mileage may vary by prompt mix and vendor readiness. A useful run ledger has four columns: product count, selected resolution, retry count, and wall-clock completion measured in your own environment. Add an operator-reviewed acceptance rate if visual quality matters. Don't collapse those into one magic score. A run that finishes quickly but sends most images back for regeneration is slow in the only sense the catalog team cares about. For moderation reports, reverse the emphasis. Send a compact, structured classification to the human-review queue quickly, using a chat model plus JSON Schema because a dedicated moderation endpoint is not supported. Humans remain the decision point. Catalog images, meanwhile, can wait for the production batch and its export.
Same application. Different clocks.
Rollout in two boring phases
At small volume, polling and a single importer are enough. At scale, I would store one immutable input manifest, one stable idempotency key, and one output manifest per run. The admin UI would read that state rather than interrogating the image provider on every page refresh. This keeps provider rate limits away from human refresh habits and makes catalog attachment replayable.
I would also split retry budgets. Transport retries handle 429 responses with backoff; content regeneration is a new catalog decision with a new cost estimate, not a hidden network retry. That distinction prevents an innocent retry loop from silently multiplying image work. Short version: retry delivery automatically, retry creative output deliberately.
At very large volume, the provider adapter deserves contract tests against discovery and fixture tests for result-to-product mapping. The rest should stay boring. A vendor change then touches the adapter, while the run ledger, progress UI, moderation queue, and catalog importer retain the same boundaries.
References
- https://platform.openai.com/docs/guides/image-generation
- https://cloud.google.com/vertex-ai/generative-ai/docs/image/overview
- https://docs.aws.amazon.com/bedrock/latest/userguide/image-generation.html
- https://developer.adobe.com/firefly-services/docs/firefly-api/
- https://python.langchain.com/docs/integrations/chat/openai/
Top comments (0)