Guide
Microsoft Graph Batch Requests Explained
Microsoft Graph JSON batching ($batch) packs up to 20 independent requests into a single HTTP round trip, but each item carries its own status code, so a batch can return 200 overall while one item fails with 429 or 404. This reference explains the per-item response model, dependsOn ordering, the 4 MB payload cap, and why the Nylas CLI removes raw $batch by handling tokens, retries, and pagination behind plain commands.
Written by Aaron de Mello Senior Engineering Manager
Command references used in this guide: nylas email list, nylas auth status, and nylas doctor.
What is a Microsoft Graph batch request?
A Microsoft Graph batch request is a single POST to /v1.0/$batch whose JSON body carries an array of up to 20 independent sub-requests, each with its own id, method, and url. Graph runs them and returns one response containing a responses array, with one entry per item. One network round trip replaces 20.
The 20-request ceiling is a hard limit documented by Microsoft, not a suggestion: a batch with 21 items is rejected outright. Sub-request URLs are relative to the Graph version root, so you write /me/messages, not the full https://graph.microsoft.com/v1.0/me/messages. According to the Microsoft Graph JSON batching docs, the combined payload must also stay under 4 MB, which constrains how many message bodies or attachments one batch can fetch.
POST https://graph.microsoft.com/v1.0/$batch
Content-Type: application/json
{
"requests": [
{ "id": "1", "method": "GET", "url": "/me/messages?$top=10&$select=subject,from" },
{ "id": "2", "method": "GET", "url": "/me/mailFolders/inbox" },
{ "id": "3", "method": "GET", "url": "/me/calendar/events?$top=5" }
]
}Why does a batch return 200 while an item fails with 429?
A Microsoft Graph batch returns HTTP 200 for the outer call as long as the request itself was well-formed, even when individual items fail. Each entry in the responses array carries its own status field. An item throttled at 429 sits inside a 200 envelope, so code that only checks the outer status silently drops failures.
Throttling is per-item because Graph applies its limits per mailbox and per resource, not per HTTP connection. A throttled item includes its own headers.Retry-After value in seconds, and Microsoft's throttling guidance requires you to honor it before resubmitting only the failed items. Microsoft notes that “a request inside a batch is evaluated against the throttling limits” the same as a standalone call, so batching does not raise your quota. The general Graph error reference covers the other codes you will see per item, and the 429 pattern traces back to RFC 6585.
{
"responses": [
{ "id": "1", "status": 200, "body": { "value": [ /* messages */ ] } },
{ "id": "2", "status": 429, "headers": { "Retry-After": "13" }, "body": { "error": { "code": "TooManyRequests" } } },
{ "id": "3", "status": 404, "body": { "error": { "code": "ResourceNotFound" } } }
]
}How does dependsOn control execution order?
By default Graph runs batched items in an arbitrary, often parallel order. The dependsOn array on a sub-request forces sequencing: an item with dependsOn: ["1"] runs only after item 1 succeeds. If the prerequisite fails, the dependent item returns 424 Failed Dependency and never executes, which keeps a half-applied write from happening.
Use dependsOn when one call must precede another, such as creating a mail folder and then moving a message into it. There is one constraint Microsoft enforces: dependent requests must form a continuous, ordered chain, so you cannot have item 3 depend on item 1 while item 2 depends on nothing. A broken chain returns 400 Bad Request for the whole batch. Ordered batches also run serially, so they trade throughput for correctness — a 20-item ordered batch is slower than 20 parallel items.
{
"requests": [
{ "id": "1", "method": "POST", "url": "/me/mailFolders",
"headers": { "Content-Type": "application/json" },
"body": { "displayName": "Receipts" } },
{ "id": "2", "method": "POST", "url": "/me/messages/{message-id}/move",
"dependsOn": ["1"],
"headers": { "Content-Type": "application/json" },
"body": { "destinationId": "Receipts" } }
]
}What are the size and count limits on a Graph batch?
A Microsoft Graph batch is bounded by three hard limits: at most 20 sub-requests, a total payload under 4 MB, and the same per-resource throttling quota that a standalone call faces. Exceeding 20 items rejects the batch; exceeding 4 MB returns a payload-too-large error. These caps mean a batch is a round-trip optimizer, not a way to raise your quota.
The 4 MB ceiling bites hardest on writes that carry bodies, such as creating messages or uploading small attachments, where a handful of items can blow the limit. To stay under it, request only the fields you need with $select and split large workloads into several batches of 20. Microsoft's Graph error reference documents the codes a rejected batch returns, and a single oversized item — not the batch as a whole — is often the cause. Counting items against the 20-request cap and summing body sizes against 4 MB before you POST avoids most rejections.
# Stay under both caps: trim fields with $select, keep items <= 20
{
"requests": [
{ "id": "1", "method": "GET", "url": "/me/messages?$top=10&$select=subject,receivedDateTime" },
{ "id": "2", "method": "GET", "url": "/me/messages?$top=10&$skip=10&$select=subject,receivedDateTime" }
]
}
# 2 of a possible 20 items; $select keeps each response body smallWhy does the Nylas CLI remove raw $batch?
The Nylas CLI removes raw $batch because the work batching exists to solve — fewer round trips, token reuse, retry handling, and pagination — already happens inside the v3 API layer. You run nylas email list and the tool fetches and paginates server-side, so there is no 20-item ceiling, no 4 MB cap, and no per-item status array to parse in your own code.
The trade-off is explicit: you give up hand-tuned batch composition and gain the removal of the auth and throttling plumbing that batching forces on you. A hand-rolled Graph client must register an Azure app, refresh tokens every 3,600 seconds, and decode per-item 429s; the CLI does all three behind one command. When something is genuinely wrong, nylas auth status and nylas doctor name the cause in plain language instead of returning a status code to decode. Contact reads run the same way — nylas contacts list --json pulls provider data directly, with no client-built batch.
# Server-side pagination replaces a hand-composed $batch of message reads
nylas email list --json --limit 20
# Plain-language diagnosis instead of parsing a per-item 429 array
nylas auth status
nylas doctor --json | jq '.checks'
# v3 pulls provider data directly, no client-built batch
nylas contacts list --jsonNext steps
- Microsoft Graph Delta Query Explained — How Graph delta query gives incremental mailbox sync
- Microsoft Graph error codes — the per-item 401/403/404/429/503 you parse inside a batch
- Microsoft Graph email quickstart — send and read Outlook mail without Azure setup
- Gmail API batch modify labels — the Google equivalent of batched writes
- Email to OneDrive — move Microsoft attachments without composing Graph calls
- Email to webhook relay — push events out instead of polling with batched reads
- Full command reference — every flag and subcommand documented