Latest
Claude Message Batches API in TypeScript: submit up to 100,000 requests at 50% off and collect results later
AI Tools for Developers

Claude Message Batches API in TypeScript (50% Off)

5 min read
Quick Answer

Batches in five lines

  • 1
    What it is → submit many Messages API requests at once and collect the results later, at 50% off both input and output tokens.
  • 2
    Limits → up to 100,000 requests or 256 MB per batch, whichever comes first.
  • 3
    Timing → most batches finish within an hour; anything not done after 24 hours expires and isn't billed.

A lot of AI work in a product doesn't need to happen while a user waits: tagging yesterday's support tickets, summarising merged pull requests, generating descriptions for a product import, re-running an eval set. For all of that, the Claude Message Batches API does the same work as the normal Messages API at half the price. The trade-off is time. You submit requests, the batch processes them in the background, and you collect results when it's done. This guide walks through a complete TypeScript job: building the requests, submitting, polling, streaming results back by ID, and handling the requests that fail.

Limits and Pricing

These are the numbers that shape how you design a batch job, from Anthropic's batch processing documentation:

  • Discount: 50% off input and output tokens on every model. Haiku 5.5 drops to $0.05 / $0.25 per million tokens, Sonnet 5.5 to $1 / $5.
  • Size: up to 100,000 requests or 256 MB per batch, whichever is reached first.
  • Speed: most batches complete within 1 hour. Results are available once everything finishes or after 24 hours, whichever comes first.
  • Expiry: requests not processed within 24 hours expire, and you aren't billed for them.
  • Retention: results can be downloaded for 29 days after the batch is created.
  • IDs: each request needs a custom_id of 1 to 64 characters using letters, numbers, hyphens and underscores.

Step 1: Build the Requests

Each batch entry is a custom_id plus the exact params you'd pass to messages.create. Make the custom_id something you can map back to your own data, such as a ticket or record ID. Results don't come back in submission order, so the ID is the only reliable link.

typescript

Turn yesterday's support tickets into batch requests, one per ticket.

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

type Ticket = { id: number; subject: string; body: string };

const SYSTEM = `Classify the support ticket. Reply with JSON only:
{"category": "billing" | "bug" | "how-to" | "feature", "urgent": boolean}`;

function toRequests(tickets: Ticket[]) {
  return tickets.map((t) => ({
    custom_id: `ticket-${t.id}`, // must match ^[a-zA-Z0-9_-]{1,64}$
    params: {
      model: "claude-haiku-5-5",
      max_tokens: 100,
      system: SYSTEM,
      messages: [{ role: "user" as const, content: `${t.subject}\n\n${t.body}` }],
    },
  }));
}

Step 2: Submit and Poll

Creating a batch returns immediately with an ID and processing_status of in_progress. Store that ID (in your database or job queue), because the job that checks on the batch may not be the process that created it.

typescript

Submit the batch, then poll until processing_status is "ended".

export async function submit(tickets: Ticket[]) {
  const batch = await client.messages.batches.create({ requests: toRequests(tickets) });
  return batch.id; // persist this
}

export async function waitForBatch(batchId: string, everyMs = 60_000) {
  for (;;) {
    const batch = await client.messages.batches.retrieve(batchId);
    const c = batch.request_counts;
    console.log(`${batch.processing_status}: ${c.succeeded} ok, ${c.errored} errored, ${c.processing} pending`);
    if (batch.processing_status === "ended") return batch;
    await new Promise((r) => setTimeout(r, everyMs));
  }
}
  • For a nightly job, you don't need a long-running poller. Submit in tonight's cron run and collect in a second cron run an hour or two later.
  • request_counts tracks processing, succeeded, errored, canceled and expired requests as the batch runs.

Step 3: Stream the Results

Results can be large, so stream them instead of downloading everything into memory. Each result carries its custom_id and one of four types: succeeded, errored, canceled or expired.

typescript

Stream results, save successes, and sort failures into retry-now and fix-first.

export async function collect(batchId: string) {
  const retry: string[] = [];
  const invalid: string[] = [];

  for await (const item of await client.messages.batches.results(batchId)) {
    const ticketId = Number(item.custom_id.replace("ticket-", ""));

    switch (item.result.type) {
      case "succeeded": {
        const block = item.result.message.content[0];
        const text = block.type === "text" ? block.text : "";
        await saveClassification(ticketId, JSON.parse(text)); // validate in real code
        break;
      }
      case "errored":
        // invalid_request_error means the request itself is wrong: fix before resending
        (item.result.error.type === "invalid_request_error" ? invalid : retry).push(item.custom_id);
        break;
      case "expired":
      case "canceled":
        retry.push(item.custom_id); // never sent to the model, not billed
        break;
    }
  }
  return { retry, invalid };
}

What a Nightly Job Costs

Take 5,000 tickets a night, each about 600 tokens in and 30 tokens out, on Haiku 5.5:

Comparison Data
modeinput costoutput costnightly total
Messages API (real time)3M tokens × $0.10/M = $0.30150K × $0.50/M = $0.075$0.375
Message Batches API3M tokens × $0.05/M = $0.15150K × $0.25/M = $0.0375≈ $0.19

When Not to Use Batches

Batches suit background work and nothing else. Skip them when:

  • A user is waiting for the answer. Use the normal Messages API, with streaming.
  • Each step depends on the previous result, as in agent loops or multi-turn tool use. A batch request is a single independent call.
  • You have only a handful of requests. The savings are real but tiny, and you add a polling step for no reason.

Frequently Asked Questions

50% off both input and output tokens compared with standard Messages API prices, on all models.
Most batches finish within an hour. Results become available when all requests are done or after 24 hours, whichever comes first, and requests not processed within 24 hours expire without being billed.
Up to 100,000 requests or 256 MB, whichever limit is reached first.
Results are available to download for 29 days after the batch is created.
Yes, and the discounts combine. Anthropic recommends the 1-hour cache duration for batches, since processing can take longer than the default 5-minute cache lifetime.

Strategic Summary

Final Thoughts

The Message Batches API is the easiest 50% saving available on the Claude API: the same request format, a custom_id per request, and a second step to collect results. Move every AI job that doesn't have a user waiting on it into a batch, add prompt caching for shared prefixes, and validate every result before you store it. Related guides: Claude prompt caching in TypeScript and Claude Haiku 5.5 for developers. --- Last reviewed: November 3, 2026. Limits, behaviour and prices are from Anthropic's batch processing and pricing documentation. No affiliation with Anthropic.

What are you running in batches? Share the job and the monthly cost, and I'll collect real numbers for a follow-up.

Get in touch

Next Up

Continue your research