Source: https://cli.nylas.com/guides/graph-api-batch-requests-explained

# 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](https://cli.nylas.com/authors/aaron-de-mello) Senior Engineering Manager

Updated June 9, 2026

> **TL;DR:** Microsoft Graph `$batch` bundles up to 20 requests into one POST to `/v1.0/$batch`. The outer call returns `200` even when an inner item fails, so you parse each item's own `status`, honor a per-item `429` with its `Retry-After`, and use `dependsOn` to force order. The catch comes later: the Nylas CLI removes the whole batching layer, and the reason is more interesting than “it's easier.”

Command references used in this guide: [`nylas email list`](https://cli.nylas.com/docs/commands/email-list), [`nylas auth status`](https://cli.nylas.com/docs/commands/auth-status), and [`nylas doctor`](https://cli.nylas.com/docs/commands/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](https://learn.microsoft.com/en-us/graph/json-batching), the combined payload must also stay under 4 MB, which constrains how many message bodies or attachments one batch can fetch.

```json
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](https://learn.microsoft.com/en-us/graph/throttling) 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](https://cli.nylas.com/guides/graph-api-error-codes) covers the other codes you will see per item, and the `429` pattern traces back to [RFC 6585](https://datatracker.ietf.org/doc/html/rfc6585).

```json
{
  "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.

```json
{
  "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](https://learn.microsoft.com/en-us/graph/errors) 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.

```json
# 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 small
```

## Why 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 `429`s; 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.

```bash
# 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 --json
```

## Next steps

- [Microsoft Graph Delta Query Explained](https://cli.nylas.com/guides/graph-api-delta-query-explained) — How Graph delta query gives incremental mailbox sync
- [Microsoft Graph error codes](https://cli.nylas.com/guides/graph-api-error-codes) — the per-item 401/403/404/429/503 you parse inside a batch
- [Microsoft Graph email quickstart](https://cli.nylas.com/guides/microsoft-graph-email-quickstart) — send and read Outlook mail without Azure setup
- [Gmail API batch modify labels](https://cli.nylas.com/guides/gmail-api-batch-modify-labels) — the Google equivalent of batched writes
- [Email to OneDrive](https://cli.nylas.com/guides/email-to-onedrive) — move Microsoft attachments without composing Graph calls
- [Email to webhook relay](https://cli.nylas.com/guides/email-to-webhook-relay) — push events out instead of polling with batched reads
- [Full command reference](https://cli.nylas.com/docs/commands) — every flag and subcommand documented
