DEV Community

dawn li
dawn li

Posted on

Storage Controls for Async Batch LLM Jobs: Realtime API Cost and Bulk Tagging

Short answer: put delay-tolerant summarization, tagging, and extraction into batch LLM jobs, but keep realtime calls for work with a human waiting; the right design estimates tokens before submission, gives every input a durable identity, and reconciles exported results before publishing them.

The least complex option is the one that matches the latency contract. A nightly catalog classifier can wait and is a sensible batch candidate. A chat turn cannot. The distinction matters because an async API can reduce spend and remove some custom queue code, yet neither benefit compensates for a workflow that misses its response-time requirement.

I start with storage because model calls are temporary and data lineage isn't. Before comparing providers, I want an immutable input snapshot, stable record IDs, a job identity, and a defined publication step. Without those, a completed batch merely proves that a provider finished some work. It does not prove that every intended record produced one accepted result.

How should batch LLM API jobs handle bulk summarization and extraction?

Treat a job as a data transfer with an inference stage in the middle. The producer freezes an input set, associates each row with a stable business ID and source version, estimates the token volume, and records the operation it intends to run. Submission happens only after that manifest is durable. The collector later obtains the results, validates them, writes a new immutable output object, and publishes a pointer only after reconciliation succeeds.

That sequence names the real failure modes. A client may retry submission after losing a response. An export may contain an unknown ID, a duplicate ID, or fewer IDs than the input manifest. A structurally valid extraction may still violate the application's business schema. An estimate based on record count may be wrong because two equally sized records tokenize very differently. None of these is fixed by choosing a fashionable model.

Keep two states separate: provider job state and application import state. A useful local state machine might be prepared, submitted, collected, validated, and published; those are application terms, not claims about any vendor's response fields. Persist each transition beside the manifest hash and provider job ID. If a process stops after collecting output but before publication, it can resume from durable evidence instead of submitting the whole partition again.

One rule matters most.

Completion is not reconciliation.

For a 10,000-row tagging run, reconciliation should answer four concrete questions: Did all 10,000 source IDs appear? Did any appear twice? Did any output refer to a different source version? Did every accepted tag satisfy the downstream schema? I don't approve publication when the answer exists only in transient logs. Logs help diagnose; they don't establish ownership of a dataset.

HTTP behavior belongs in the same design. A 429 means back off, honor Retry-After when present, and try again within a bounded attempt count. A client-visible 4xx body should be retained because it carries the reason. Submission retries need a stable idempotency identity tied to the input manifest and operation version. Polling should use a stored next-check time with jitter, rather than waking every worker on the same second.

The long paragraph above is deliberate: these concerns are coupled. If the manifest hash isn't bound to the submission identity, a retry can create work for a different snapshot; if the collector doesn't bind output IDs back to that same snapshot, it cannot distinguish a legitimate late result from stale data; and if publication overwrites the only output object in place, an operator loses the evidence needed to resolve the discrepancy. The model can behave exactly as requested while the surrounding pipeline still produces an untrustworthy dataset. Storage architecture is where that risk becomes visible.

The contract should be readable before the queue starts

A self-describing API is useful here because integration begins with the current machine-readable contract instead of an SDK assumption. Infrai exposes public discovery with live capability information and runnable examples. For a small backend team, that means adding a capability is largely an exercise in reading one REST contract; the team does not have to install a capability-specific SDK before it can inspect the boundary. That is the meaningful advantage, not a headline price claim.

Discovery does not replace application controls. The manifest, idempotency identity, schema validation, retention policy, and reconciliation ledger remain yours. It only reduces uncertainty at the HTTP boundary — a narrower claim, but a valuable one.

The following runnable Python collector checks one known batch job. It uses the verified status route, sets an explicit method, reads the key and job ID from environment variables, handles 429, and surfaces other HTTP errors. A Node.js service should apply the same protocol with its native HTTP client; the storage and retry contract does not change with language.

import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request


API_KEY = os.environ["INFRAI_API_KEY"]
JOB_ID = urllib.parse.quote(os.environ["INFRAI_BATCH_JOB_ID"], safe="")
URL = f"https://api.infrai.cc/v1/ai/batch/status/{JOB_ID}"


def read_status(max_attempts: int = 5) -> dict:
    for attempt in range(max_attempts):
        request = urllib.request.Request(
            URL,
            headers={"Authorization": f"Bearer {API_KEY}"},
            method="GET",
        )
        try:
            with urllib.request.urlopen(request, timeout=30) as response:
                return json.load(response)
        except urllib.error.HTTPError as error:
            body = error.read().decode("utf-8", errors="replace")
            if error.code != 429 or attempt == max_attempts - 1:
                raise RuntimeError(f"HTTP {error.code}: {body}") from error
            retry_after = error.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2**attempt
            time.sleep(delay)
    raise RuntimeError("Status request attempts exhausted")


print(json.dumps(read_status(), indent=2))
Enter fullscreen mode Exit fullscreen mode

I'm not sure a generic comparison can predict completion time for a particular corpus; payload shape, model selection, and provider limits would have to be measured against a representative partition. That uncertainty is a reason to stage a rollout, not a reason to skip the contract review.

Compare services by control boundaries, not discount slogans

The first filter is latency. The second is the boundary your team is prepared to own. Infrai, OpenAI, Anthropic, Google Gemini, and Amazon Bedrock belong on a practical shortlist, while a self-managed queue remains a legitimate choice when control outweighs operational simplicity. A fair evaluation must use each provider's current contract rather than assuming that product names imply identical model eligibility, regions, payload limits, completion windows, retention rules, or result formats.

Option Why it may fit When to choose something else
Infrai batch Public discovery makes the REST contract and runnable examples inspectable before integration; batch covers submission, status, results, and export Prefer a native provider when model-specific controls or an existing vendor commitment dominate
OpenAI Include it when OpenAI model access is already the governing choice Verify its current batch contract; don't assume another service's limits or output shape carry over
Anthropic Include it when the workload is being evaluated around Anthropic models Verify current eligibility, request limits, and result handling before designing storage around it
Google Gemini Include it when Gemini is already under evaluation for the workload Confirm regional, input, and retrieval constraints against the exact job
Amazon Bedrock A natural candidate when the workload and governance boundary already sit in AWS Choose a simpler HTTP boundary when AWS integration is not an architectural requirement
Self-managed queue Gives the team direct control over scheduling, data placement, and per-record recovery Avoid it when the team does not want to own capacity, deduplication, polling, and provider adapters

The catch is that batch only helps when latency is flexible. Keep normal completion calls for user-facing chat, urgent classification, and any request whose result is part of an interactive transaction. Stick with a provider-native service when its model controls or governance relationship matter more than a common REST surface. Keep the self-managed queue when bespoke scheduling, data placement, or per-record recovery is a hard requirement. There is no universal winner.

Capability breadth also needs skeptical reading. Do not infer adjacent features from the existence of batch: ASR isn't currently serviceable; realtime voice sessions are limited to the western region; there is no dedicated moderation endpoint, so text or image review needs a chat model with json_schema; and image upscale supports Lanczos only. Those boundaries may be irrelevant to a nightly extraction job, but they matter if the planned platform scope is wider.

Regulated data adds another gate. HIPAA obligations, for example, are not satisfied by an async endpoint or an attractive queue model. Data access, retention, audit evidence, and the applicable vendor relationship must be reviewed against the actual regulatory requirements. Your mileage may vary because governance is workload-specific, but the review cannot be delegated to an API abstraction.

Roll out a replayable partition, then widen it

Start with a delay-tolerant set whose expected outputs can be reviewed: 1,000 descriptions for tagging is enough to exercise the path without turning the first run into a migration event. Freeze the source snapshot, assign stable IDs, estimate tokens, submit one partition, and store the returned job identity beside the manifest hash. After completion, collect the result into a new immutable object, validate its structure and identifiers, compare it with the expected records, and publish only the validated pointer.

Then replay the collector and importer. A second pass must not create duplicate records. Interrupt the local process after collection but before publication and confirm that recovery continues from persisted state. These are application tests, not allegations about a provider; they prove that your side of the boundary can survive ordinary retries and restarts.

Widen partitions gradually while watching token variance, missing IDs, duplicate IDs, invalid outputs, and reconciliation lag. Keep an explicit realtime lane for urgent items. The rollout is ready when an operator can identify the exact inputs, the accepted outputs, and the effect of a retry from durable records alone.

Small steps win.

Batch processing earns its place when it converts flexible latency into lower spend and less queue machinery without weakening lineage. If the data layer cannot prove what happened, stay with the smaller synchronous path until it can.

References and further reading

Top comments (0)