Batch and streaming are often presented as two ways to generate a response. They solve different problems.
Streaming sends a response to the user as it is generated. Batch processing groups independent requests, runs them asynchronously, and returns results later. One optimizes when the user sees the first useful output; the other optimizes how efficiently a system processes work that does not need an immediate answer.
Choosing the wrong one creates predictable failures. Batch output in a chat box feels broken because the user waits with no response. Streaming a million back-office classifications creates connection management and per-request overhead where a queued job would be simpler. This article compares the two modes, explains the operational trade-offs, and shows how to combine them in a single product.
They are different axes, not interchangeable modes
Streaming changes how a single inference result is delivered. The application receives incremental events—often through server-sent events—and can render text or process tool-call updates before the response is complete.
Batch inference changes how many requests are submitted and when they are executed. The application submits a set of independent requests, tracks a job, and retrieves results when processing finishes. It does not imply that results stream to the end user.
These options can be combined with other decisions:
| Decision | Examples |
|---|---|
| Interaction model | Synchronous request, background job, queued workflow |
| Result delivery | One response body, streamed events, later notification |
| Work grouping | One request, micro-batch, large batch |
| User expectation | Immediate answer, progress update, eventual completion |
A user-facing app can stream an interactive answer while using batch processing overnight to classify the conversation archive. A background job can produce an answer without streaming and notify the user when it is ready. Don’t decide based on the words “real-time” and “batch” alone; identify the unit of work and the user’s deadline.
Streaming buys early feedback, not a faster computation
Without streaming, the client generally waits for the full generated response before receiving the body. Streaming emits events as they become available, so the UI can show the first tokens earlier. The model still needs to process the prompt and generate the full response. A long tool call or retrieval step may happen before the first visible token, and streaming does not shorten that work.
The useful latency measures are:
- Time to first event: when the server acknowledges or begins the response stream.
- Time to first useful token: when the user sees meaningful content, not a preamble.
- Time to completion: when the response and any required checks are complete.
- Time to committed outcome: when a downstream action is confirmed.
If a model streams “I’m checking that now” before spending six seconds on an API call, the perceived delay may improve slightly, but the actual task time has not. If the first token is generic filler, the experience may feel faster while providing no value.
OpenAI’s current Responses API streaming guide uses semantic server-sent events and documents lifecycle and content events such as response.created, response.output_text.delta, response.completed, and error. The event stream is more than text chunks: it can include tool-call argument deltas, refusal events, and other activity. (OpenAI, streaming API responses)
Batch optimizes throughput, cost, and decoupling
Batch processing is useful when requests are independent and the result is not needed in the current interaction. It can improve throughput, avoid holding user connections open, and offer different pricing or rate-limit characteristics depending on the provider.
The current OpenAI Batch API documentation describes a 24-hour completion window, a 50% lower cost than synchronous APIs, and a separate pool of higher rate limits. It lists use cases such as evaluations, large-scale classification, and embedding repositories. Input is provided as JSONL, each request needs a custom_id, and output order may differ from input order; results should be joined by ID. (OpenAI, Batch API)
Those details are provider-specific and can change. The architectural point is broader: batch makes the job asynchronous. You must persist the input-to-result mapping, track status, handle partial completion, and notify or expose results later.
1. Pick the delivery mode from the user’s deadline
Ask what the user is waiting for:
- The first useful sentence: stream if partial text is safe and useful.
- A complete, validated answer: wait for completion or stream behind a “checking” state.
- A business action: show a pending state and confirm the side effect; token streaming is secondary.
- A large analysis: accept the request as a job and provide status later.
- An offline transformation: batch it and let the user retrieve the result when ready.
An interactive assistant often needs a hybrid response. It can acknowledge immediately, stream a preliminary explanation, then show a clear completed state after tool calls and validation finish. If an output guardrail or policy check must run on the full response, be honest about what is provisional. Do not present a partial answer as final and then silently revise it.
For a slow task, a queue is usually better than holding an HTTP connection until the provider finishes. The UI can poll or subscribe to task status, and the worker can resume after a deploy. Streaming a long-running job through a single fragile connection makes reconnect and cancellation more complex.
2. Use batch for independent work, not dependent workflows
Batch is a natural fit for:
- classifying a corpus of support messages;
- generating embeddings for a document set;
- running offline evaluations across many cases;
- producing first-pass tags for records that will later be reviewed;
- bulk enrichment where each input can be processed independently.
It is a poor fit when request N depends on the result of request N−1, when the user needs to correct an intermediate step, or when an action must occur immediately after an approval. Those are workflow or queue problems, not a single batch submission.
If a batch contains multiple item types, keep each item independently addressable and recoverable. A batch job can complete with some requests failed or expired. The application should process successful items, surface the errors, and retry only those items with a safe identity. Do not assume a batch is all-or-nothing.
3. Budget for streaming’s connection lifecycle
Streaming introduces a long-lived connection between client and server. Proxies, mobile networks, browser tabs, and load balancers can disconnect. The client must handle disconnects, event parsing, duplicate events if reconnect is supported, and terminal errors that arrive after partial content.
Design the contract:
- Is a reconnect supported, or does the client restart the request?
- Is the response stored so the client can retrieve the final result later?
- Does cancellation stop only the client stream, or the provider operation too?
- Can a tool call occur before stream completion?
- How does the UI mark content as partial or unverified?
- What is the state if the stream ends without a completion event?
Streaming does not automatically provide resumability. If the connection drops, the client may not know whether the model finished or whether a tool executed. Use a stable operation ID and store the final task state server-side. Avoid blindly restarting a side-effecting workflow when a stream is lost.
4. Batch requires durable bookkeeping
The batch file is not your database. Persist a job record with:
- batch ID and provider;
- input artifact ID and hash;
- custom IDs mapped to internal task IDs;
- model and prompt versions;
- creation time and deadline;
- status, completed count, and failed count;
- output and error file references;
- retention and cleanup status.
Use custom IDs as the join key rather than relying on result order. Handle partial completion, expired jobs, and duplicate result processing idempotently. If a batch result is ingested twice, it should not create duplicate records or repeat downstream actions.
The current OpenAI Batch API docs say uploaded input uses JSONL, a batch can contain up to 50,000 requests and a 200 MB input file, and output order may not match input order. They also document that output files are automatically deleted 30 days after completion. That makes file lifecycle and result persistence part of the application design, not incidental details. Verify current limits and retention for your account and endpoint before shipping. (OpenAI, Batch API)
5. Streaming complicates moderation and structured output
If the application must evaluate the entire output before showing it, streaming reveals content before that evaluation is complete. OpenAI’s streaming documentation specifically notes that moderation is harder with partial output; when moderation scores are requested alongside a generation, they arrive after the full output, not with each delta. (OpenAI, streaming API responses)
Options include:
- buffer the full answer before display for high-risk content;
- stream only a low-risk draft and clearly label it provisional;
- stream from a constrained subset of tasks with output checks after completion;
- use deterministic output constraints where possible;
- stop rendering if a later event marks the response failed or refused.
Structured output can also be awkward to display incrementally. A partially emitted JSON object is not a valid object. The UI should not parse or execute incomplete fragments. Wait for the completed structured result, validate it, then render the fields. For user-facing prose, incremental display is more natural, but it still needs a completion state.
6. Compare the economics using total system cost
Batch processing may have a lower per-token rate, but the total cost also includes:
- storage for input and output files;
- orchestration and polling;
- failed or expired requests;
- retries and reconciliation;
- human review of lower-confidence results;
- the product cost of delayed freshness.
Streaming may cost the same per token as a non-streaming request for a given provider, but it can reduce user abandonment or avoid holding a worker while waiting. It may also encourage longer outputs because users see the model continuing. Measure cost per successful task and time-to-useful-result, not only the provider’s input-token price.
Use a batch when its asynchronous contract is acceptable and its throughput or price advantage is real for your workload. Use streaming when early partial content improves the interaction enough to justify connection complexity and the moderation implications. Use neither as a substitute for designing a durable state machine around writes.
7. Cancellation means different things in each mode
For a stream, the user may close the tab while generation continues on the server. If the provider supports cancellation and the application propagates it, the work may stop; otherwise, the client has only stopped receiving data.
For a batch, canceling the overall job may not instantly stop in-flight requests. The current OpenAI Batch API documentation states that cancellation can remain in cancelling while in-flight requests complete, for up to 10 minutes. Completed responses may still be available. The UI should distinguish “cancellation requested” from “cancelled,” and the ingestion worker must handle results that arrived during cancellation.
For a workflow with external actions, cancellation after a tool call may require reconciliation or compensation. Do not label it “cancelled” if a payment, message, or record update might already have succeeded.
8. Handle failures differently
Streaming errors can occur before the first event, mid-stream, or after tool execution. The client needs to preserve partial output and show whether the operation completed. Retrying an entire call may duplicate costs or effects.
Batch failures occur per request or at the job level. Input validation can reject a whole batch; individual lines can fail; a batch can expire with unfinished requests. Use an error file or per-item status to retry only eligible requests, and make the retry idempotent.
In both modes, classify errors. Authentication errors, invalid input, exhausted spend limits, and temporary overload should not all receive the same “try again” action. Retry transient errors within a budget, surface terminal conditions, and retain a path for unknown outcomes.
9. A hybrid design for a user-facing product
Many applications should use both modes:
User asks a question
→ synchronous request with streaming for the short answer
→ tools and policy checks complete before any write
→ long follow-up analysis becomes a background task
Nightly job
→ batch classify completed conversations
→ ingest per-item results by custom ID
→ review failures and update the evaluation set
The synchronous path serves the user’s immediate need. The batch path handles independent, lower-priority work without competing for interactive latency. They can share a prompt and model family, but they should have distinct budgets, status tracking, and error policies.
Avoid putting a batch job in the middle of an interactive request. If the user is waiting for an answer, submit to a batch API and then poll for up to 24 hours is not a user experience; it is a background workflow that needs an explicit product surface.
Streaming has infrastructure requirements beyond the SDK
The model SDK can expose an async iterator, but the bytes still pass through your application server, reverse proxy, load balancer, and browser. Any layer that buffers the full response defeats the perceived-latency benefit. Check proxy buffering, idle timeouts, maximum connection duration, and whether your hosting platform supports long-lived responses.
The server should propagate cancellation and apply backpressure. If the browser is gone, stop doing work when the provider and workflow allow it. If it cannot be stopped, persist the operation and let the user retrieve the result later. Do not leave a worker blocked on a disconnected client indefinitely.
Use semantic event types rather than treating every frame as text. A stream may contain tool-call events, refusal events, completion events, or errors. The UI should render only the event types intended for users; internal tool arguments and intermediate data should not leak just because they appeared on the wire.
Test through the actual production ingress path. A local for await loop can work perfectly while a proxy waits for a buffer to fill or closes the connection before the model completes. Test on mobile networks and with a client that navigates away, not only on a stable desktop connection.
Batch savings must include the lifecycle around the job
The Batch API currently documents a 50% price discount relative to synchronous APIs, higher separate rate limits, and a 24-hour completion window. Those are useful properties for eligible workloads, but they do not guarantee that your total feature becomes 50% cheaper. You still pay for orchestration, file storage, retries, failed records, reviewing results, and delayed value. (OpenAI, Batch API)
The batch input and output are also stored artifacts. The documented output file is automatically deleted after 30 days, so the application must persist results it needs longer and enforce its own retention policy. Do not leave sensitive request files in a shared bucket because the provider’s file lifecycle was misunderstood.
Batch has its own capacity limits, including per-batch request and file-size limits, queued prompt-token limits, and batch creation limits. Treat them as current provider-specific constraints and verify them in the docs and project settings before building a scheduler around them. Chunk large jobs into recoverable units, assign stable custom_id values, and keep a manifest so an expired job can resume only its unfinished items.
Calculate cost per completed, accepted item. If 10% of a batch expires, 5% of outputs fail validation, and 15% of valid results require expensive human correction, the headline discount is not the economics your product experiences. Track success and correction by item and by batch.
Use provider pricing and limits as deployment-time configuration, not assumptions buried in the job runner. Recheck them when endpoint support, retention, or model availability changes.
A decision table
| Requirement | Streaming | Batch |
|---|---|---|
| User needs visible output now | Strong fit | Poor fit |
| Independent high-volume requests | Possible, but operationally chatty | Strong fit |
| Partial output has value | Strong fit | Not applicable to user interaction |
| Full-result validation before display | Buffer or avoid streaming | Natural, since result arrives after completion |
| Long processing time | Fragile for long-lived connection | Better with durable job tracking |
| Per-item status and retry | Requires operation tracking | Required for partial batch outcomes |
| User can correct mid-task | Interactive workflow needed | Usually poor fit |
| Cost efficiency | Depends on provider and workload | May be better for eligible workloads |
Before choosing, answer these questions
- Does the user need the first token, a complete answer, or a completed action?
- Can partial output be safely shown before full validation?
- Are requests independent, or do they depend on prior results?
- What is the acceptable deadline, and what happens after it?
- Can the client reconnect or retrieve final state after disconnect?
- How are batch items mapped to internal tasks and retried?
- What happens to incomplete work on cancellation or expiration?
- How are moderation, structured-output validation, and tool writes handled?
- What are the total cost and latency under realistic traffic?
- Does the UI communicate pending, partial, completed, failed, and unknown states?
Streaming is the right choice when incremental feedback helps the user and the application can handle partial content honestly. Batch is right when work is independent and the user can wait for a durable result. In a mature product, both may be present: streaming for the conversation, batch for the backlog. The design should follow the task’s deadline and consequences, not the convenience of whichever API looks easiest in a prototype.