Throttled fan-out (default)

Batch & Throttled Fan-out diagramTickets, Rate limiter, Worker 1, Worker 2, Worker 3, Results. Step captions follow the diagram.Tickets10,000 itemsRate limiter500 RPM, 200k TPMWorker 1Worker 2Worker 3ResultsBatch & Throttled Fan-out diagramTickets, Rate limiter, Worker 1, Worker 2, Worker 3, Results. Step captions follow the diagram.Tickets10,000itemsRatelimiter500 RPM,200k TPMWorker 1Worker 2Worker 3Results

All steps
Parts of this pattern
Tickets Input / output
10,000 support tickets to classify into 12 categories overnight; each one is independent of the others.
Rate limiter Guard
Plain code: token buckets for requests and tokens per minute. A request goes out only when both buckets have room.
Worker 1 LLM call
One concurrent call to the model, made by one of a fixed pool of code workers: it takes the next ticket and returns its label.
Worker 2 LLM call
A second concurrent call to the same model. When the provider pushes back with a 429, its ticket goes back to the queue.
Worker 3 LLM call
A third concurrent call to the same model. Real runs keep tens in flight; the limiter, not the pool size, sets the pace.
Results Aggregator
Collects one label per ticket, keyed by ticket id, written to disk as each reply arrives so a crash loses nothing.
Tickets → Rate limiter
Tickets leave the queue only as fast as the limiter allows.ticket 4512: 'App crashes when I upload a photo larger than 10 MB.' (~180 tokens)
Rate limiter → Worker 1
When both buckets have room, the next ticket goes to a free worker.bucket: 312/500 requests, 141k/200k tokens this minute; dispatching ticket 4512
Rate limiter → Worker 2
Another ticket goes to the next free worker, in parallel.dispatching ticket 4513
Rate limiter → Worker 3
A third ticket goes out in parallel.dispatching ticket 4514
Worker 2 → Rate limiter429
A rate-limit error sends the ticket back through the limiter after a backoff, and briefly slows the whole pool.ticket 4513: HTTP 429; retry 1 of 5 in 4 s; pool paused 4 s
Worker 1 → Results
Each worker's label is written under its ticket's id as soon as it arrives.{"id": "4512", "category": "bug: uploads", "confidence": 0.94}
Worker 2 → Results
The retried ticket's label arrives later; order does not matter because results are keyed by id.{"id": "4513", "category": "billing", "confidence": 0.88}
Worker 3 → Results
The third worker's label joins the results.{"id": "4514", "category": "feature request", "confidence": 0.79}

Provider batch

Submit every request in one asynchronous batch job; the provider runs it within a day at half price, with no per-minute throttling on your side.

Batch & Throttled Fan-out: Provider batch diagramTickets, Batch API, Results, Labels. Step captions follow the diagram.Tickets10,000 itemsBatch APIasync, -50% costResultsLabelsBatch & Throttled Fan-out: Provider batch diagramTickets, Batch API, Results, Labels. Step captions follow the diagram.Tickets10,000itemsBatch APIasync,-50% costResultsLabels

All steps
Parts of this pattern
Tickets Input / output
The same 10,000 tickets, turned into 10,000 requests, each tagged with its ticket id.
Batch API Tool
The provider's batch endpoint. It accepts the whole set at once, runs it when it has capacity, and finishes most batches within an hour.
Results Aggregator
The downloaded results, matched back to tickets by custom id; errored or expired requests are picked out for resubmission.
Labels Input / output
One category per ticket, ready for the morning report.
Tickets → Batch APIsubmit
All requests go to the provider in one job; there is no rate limiter or worker pool to run.POST /batches: 10,000 requests, custom_id 'ticket-0001' ... 'ticket-10000'
Batch API → Resultswhen done
When the batch has ended, its results are downloaded in one file.status: ended. succeeded 9,987, errored 13, expired 0. Results file ready.
Results → Ticketserrored
Requests that errored or expired go back as a new, smaller batch.Resubmitting 13 errored requests as batch 2.
Results → Labels
Every succeeded result becomes a label on its ticket.{"custom_id": "ticket-4512", "category": "bug: uploads"}

← → neighboursesc atlas

Efficiency & Cost

Batch & Throttled Fan-out

Also known as Batch API · Async fan-out · Rate-limited parallelism · Bulk inference

Run thousands of independent requests through a rate limiter with retries, or submit them as one provider batch at half price.

Throttled fan-out: No single inventor · see source

No single inventor: rate-limited worker pools are standard engineering. The OpenAI Cookbook's parallel processor (added January 2023) popularized throttling requests and tokens per minute with retries for LLM APIs.

Provider batch: Popularized by OpenAI and Anthropic (2024)

No single inventor: the OpenAI Batch API (April 2024) and the Anthropic Message Batches API (October 2024) made asynchronous bulk inference at a 50% discount, within 24 hours, a standard option.

Measured over one run

Throttled fan-out

Nodes
6. Boxes in the diagram: models, tools, stores, people and plain code.
LLM calls
3. Model invocations in the first scenario: each time a model or agent node starts thinking, plus each branch it decides.
Tool calls
0. Messages sent to a tool node in the first scenario.
Steps
3. Steps in the first scenario, with repeated steps counted every time they run.
Scenarios
1. Distinct example runs you can switch between on the diagram.
Loop
yes. Whether the flow cycles back on itself until a condition is met.
Parallel
yes. Whether two or more messages are in flight at the same time.
Branch
no. Whether the flow takes one path out of several, chosen at run time.
Human in the loop
no. Whether a person appears as a node, to approve, correct or answer.
Autonomy
low. How much the model decides the control flow itself, from fixed code paths (low) to open-ended (high).
Cost
low. Relative token spend per request.
Latency
high. Relative wall-clock time per request.

Provider batch

Nodes
4. Boxes in the diagram: models, tools, stores, people and plain code.
LLM calls
0. Model invocations in the first scenario: each time a model or agent node starts thinking, plus each branch it decides.
Tool calls
2. Messages sent to a tool node in the first scenario.
Steps
4. Steps in the first scenario, with repeated steps counted every time they run.
Scenarios
1. Distinct example runs you can switch between on the diagram.
Loop
yes. Whether the flow cycles back on itself until a condition is met.
Parallel
no. Whether two or more messages are in flight at the same time.
Branch
no. Whether the flow takes one path out of several, chosen at run time.
Human in the loop
no. Whether a person appears as a node, to approve, correct or answer.
Autonomy
low. How much the model decides the control flow itself, from fixed code paths (low) to open-ended (high).
Cost
low. Relative token spend per request.
Latency
high. Relative wall-clock time per request.

Counted from the first scenario's timeline when the site is built.

Problem

Some jobs are not one hard request but thousands of easy ones: classify 10,000 support tickets into 12 categories, extract fields from every contract in an archive, grade an evaluation set. A simple loop that calls the model one item at a time would take all night. Fire them all at once and the provider answers with a wall of 429 rate-limit errors, half the items fail, and the script crashes with no record of which ones finished.

How it works

The default tab shows a throttled fan-out that you run yourself. The tickets wait in a queue. A rate limiter keeps two token buckets, one for requests per minute and one for tokens per minute, set just below the account’s limits, and releases a ticket only when both have room. A pool of concurrent workers (worker 1, worker 2, worker 3 here, often dozens in practice) each take the next ticket, call the model, and send its label to the results.

When the provider still pushes back, as with worker 2’s 429 in the example, the ticket goes back through the limiter after an exponential backoff with jitter, and the whole pool pauses briefly so the other workers do not hit the same wall. Every result is written to disk under its ticket id as soon as it arrives, so the order in which tickets finish does not matter, and a restarted run can skip what is already done.

If nobody needs the answers for a few hours, there is a simpler and cheaper option: the provider batch tab. Most providers accept a whole file of requests as one asynchronous job, run it when they have spare capacity, and charge about half the normal price. Anthropic’s documentation says most batches finish within an hour, with a 24-hour limit. You submit, poll, download, and resubmit the few requests that errored.

When to use

  • You have many independent items: classification, extraction, tagging, summarizing, evaluation runs.
  • Nobody is waiting on any single item. Use the provider batch when results can take hours; use the throttled fan-out when they are needed in minutes.
  • Cost or rate limits, not model quality, are the bottleneck.

When to avoid

  • Items depend on each other, so one result feeds the next. Use a chain or an agent instead.
  • A person is waiting for each answer. Serve those requests directly.
  • There are only a handful of items; a plain loop is easier to read and debug.

Trade-offs

The throttled fan-out is as fast as your rate limits allow, but you own the limiter, the retries and the bookkeeping, and you pay full price. The provider batch halves the cost and removes the per-minute throttling, though batch APIs still limit batch size and queued requests, but you give up control of timing: results arrive when the provider is done, and a busy period can push a batch towards its deadline. Both trade per-item latency for throughput, and both need results matched by id rather than by order.

Failure modes

  • Retry storm: every worker retries at once and keeps tripping the limit. Back off with jitter and pause the whole pool on a 429.
  • Lost progress: the script crashes at item 6,000 and starts over, paying twice. Write results incrementally and skip done items on restart.
  • Mismatched results: results are matched to items by position and one failure shifts every label by one. Always carry and match an id.
  • Silent failures: a few hundred items error and nobody notices. Report succeeded and failed counts and keep an errors file.
  • Duplicate batches: a restarted script submits the same batch again. Save batch ids as soon as they are created.

Build it with your AI

Throttled fan-out

Paste this into Claude Code, Cursor, ChatGPT or any AI coding assistant.

Implement the Batch & Throttled Fan-out pattern in this codebase, using the LLM client and language we already use.

## Goal

Process a large list of independent items through a model as fast as the account's rate limits allow, retrying transient failures, and collect one result per item.

## How it works

1. Load the items into a queue, each with a stable id, and build each request from the Per-item classification prompt.
2. Estimate each request's token count, and run a limiter with two token buckets: requests per minute and tokens per minute, set just below your account's limits.
3. Start a fixed pool of concurrent workers. Each takes the next item, waits until the limiter has capacity for it, and calls the model.
4. On a 429 or 5xx, put the item back in the queue with exponential backoff and jitter, and pause the whole pool briefly so the other workers do not hit the same limit.
5. Parse each reply and write it to the results file under the item's id as soon as it arrives; record items that fail permanently in a separate errors file.

## Prompts

### Per-item classification prompt

```markdown
Classify the support ticket into exactly one category.

<categories>
{{categories}}
</categories>
<ticket id="{{ticket_id}}">
{{ticket_text}}
</ticket>

Reply in JSON: {"id": "{{ticket_id}}", "category": "<one category>", "confidence": 0.0-1.0}
```

## Stop rule

Stop when the queue is empty and no worker is busy. Items that fail 5 times go to the errors file with their last error; the run reports counts of succeeded and failed items.

## Requirements

- Make the request and token limits, the concurrency and the retry cap parameters.
- Write results incrementally, keyed by item id, so a crashed run can skip items that are already done when restarted.
- Never let results depend on completion order: match each reply to its item by id.
- Log throughput (items and tokens per minute), the 429 rate and the total cost as the run progresses.
- Add a test with a stub model that returns 429 on the first call for item 7 and succeeds otherwise; for 20 items, assert 20 results, that item 7 was called twice, and that every result carries the right id.

Provider batch

Paste this into Claude Code, Cursor, ChatGPT or any AI coding assistant.

Implement the Batch & Throttled Fan-out pattern (Provider batch variant) in this codebase, using the LLM client and language we already use.

## Goal

Process a large list of independent items through the provider's asynchronous batch API at the batch discount, and match every result back to its item.

## How it works

1. Build one request per item from the Per-item classification prompt, each tagged with a custom id equal to the item's id.
2. Split the requests into batches under the provider's limits on count and size, and submit each batch.
3. Poll each batch's status at a modest interval (every few minutes) until it has ended.
4. Download the results and match each one to its item by custom id; never by position.
5. Collect the requests that errored or expired and submit them again as a new, smaller batch.

## Prompts

### Per-item classification prompt

```markdown
Classify the support ticket into exactly one category.

<categories>
{{categories}}
</categories>
<ticket id="{{ticket_id}}">
{{ticket_text}}
</ticket>

Reply in JSON: {"id": "{{ticket_id}}", "category": "<one category>", "confidence": 0.0-1.0}
```

## Stop rule

Stop when every item has a succeeded result, or after 2 resubmissions. Items still failing go to an errors file with their last error.

## Requirements

- Persist the batch ids as soon as they are created, so a restarted script polls existing batches instead of submitting duplicates.
- Treat a batch that is still running after the provider's deadline as expired and resubmit only its unfinished items.
- Log each batch's id, request count, status changes, and the counts of succeeded, errored and expired results.
- Add a test with a stub batch client that returns results out of order with one errored item; assert every result is matched to the right item and exactly one item is resubmitted.

Prompt templates

Throttled fan-out · Provider batch

Per-item classification prompt
Classify the support ticket into exactly one category.
<categories>
{{categories}}
</categories>
<ticket id="{{ticket_id}}">
{{ticket_text}}
</ticket>
Reply in JSON: {"id": "{{ticket_id}}", "category": "<one category>", "confidence": 0.0-1.0}

Related patterns

Composes well with:

Sources

  1. OpenAI Cookbook: api_request_parallel_processor.py
  2. Anthropic docs: Batch processing
  3. Introducing the Message Batches API (Anthropic, 2024)

From the Agentic Atlas by Joran Kikke · Edit on GitHub · Suggest a correction