Processing hundreds or thousands of SharePoint items in Power Automate inevitably leads to throttling. You can configure parallelism carefully, add delays, and implement retry logic, but these approaches treat the symptom rather than the cause. The root issue is that you're making hundreds of individual API calls when SharePoint's batch API lets you bundle multiple operations into a single request. This isn't a small optimization. In practice, batching often runs 5 to 100 times faster than a per-item loop, and it collapses hundreds of API calls into one request, which is what really keeps you under the throttling limits.
Batch operations require understanding SharePoint's OData batch format and building HTTP requests by hand rather than using the convenient connector actions. The complexity is real, but for flows that regularly process large datasets, the reliability and performance gains make it worthwhile.
Why Individual Calls Don't Scale
When you use SharePoint connector actions like the "Create item" action inside an "Apply to each" loop, each iteration makes a separate API call. With concurrency enabled, "Apply to each" runs up to 50 iterations in parallel (the maximum; the default when you switch concurrency on is 20), so a busy flow fires calls quickly. The SharePoint connector allows 600 calls per connection every 60 seconds, and that budget is shared across every flow that uses the same connection, so it's easy to cross.
Even with concurrency off (sequential processing), creating 600 items takes 600 API calls. If your flow runs several times an hour or processes thousands of items, you'll hit throttling no matter how carefully you tune parallelism. The connector limit is only one of several layers, since SharePoint's own service throttling and the per-license Power Platform request limits apply too. I cover all of those in detail in API Rate Limits and Throttling in Power Automate.
Batch operations change this equation. Instead of 600 API calls for 600 items, you make a handful of batch requests, or even a single request when your item count is small enough. That reduces API consumption dramatically and is usually the difference between a flow that throttles and one that doesn't.
SharePoint Batch API Basics
SharePoint's batch API accepts a specially formatted HTTP request that contains multiple operations. The format uses OData batch syntax with multipart MIME boundaries separating the individual requests inside the batch. Read operations sit directly in the batch, while write operations (create, update, delete) must be wrapped in a changeset, which is a nested multipart block with its own boundary.
A batch that creates two items looks like this:
POST https://<insertTenant>.sharepoint.com/sites/<insertSite>/_api/$batch
Content-Type: multipart/mixed; boundary=batch_<guid>
--batch_<guid>
Content-Type: multipart/mixed; boundary=changeset_<guid>
--changeset_<guid>
Content-Type: application/http
Content-Transfer-Encoding: binary
POST https://<insertTenant>.sharepoint.com/sites/<insertSite>/_api/web/lists/getbytitle('Documents')/items HTTP/1.1
Content-Type: application/json;odata=verbose
{"__metadata": {"type": "SP.Data.DocumentsListItem"}, "Title": "Item 1"}
--changeset_<guid>
Content-Type: application/http
Content-Transfer-Encoding: binary
POST https://<insertTenant>.sharepoint.com/sites/<insertSite>/_api/web/lists/getbytitle('Documents')/items HTTP/1.1
Content-Type: application/json;odata=verbose
{"__metadata": {"type": "SP.Data.DocumentsListItem"}, "Title": "Item 2"}
--changeset_<guid>--
--batch_<guid>--
Each operation inside the batch is a complete HTTP request, with its own method, headers, and body. Two details trip people up. Every part needs both the "Content-Type: application/http" and the "Content-Transfer-Encoding: binary" headers, and the boundary formatting (including the blank lines) has to be exact or SharePoint returns a generic 400. The write body also needs the correct list "type" in its metadata, or the item won't save. Each inner request line uses the full absolute URL and must target the same site as the batch endpoint, since relative paths are a common cause of a 400.
Many community examples drop the changeset and list write operations directly in the batch, and SharePoint often accepts that. The changeset form above is the shape Microsoft documents, and it's the one I'd start from.
Implementation Pattern in Power Automate
Building the batch by hand sounds painful, but there is a clean pattern that avoids looping entirely. You shape every item with a "Select" action, then join the results into one body.
Get the list's entity type first. The write body needs the list's exact type name, and you should never guess it. Ask SharePoint for it once with a "Send an HTTP request to SharePoint" action:
Method: GET
Uri: /_api/web/lists/getbytitle('Orders')?$select=ListItemEntityTypeFullName
Accept: application/json;odata=nometadata
The type is built from the list's internal name, which is frozen when the list is created. A list made as "Orders" and later renamed to "Customer Orders" still has the type "SP.Data.OrdersListItem", and spaces become "x0020", so the display name is a poor guess. Reading "ListItemEntityTypeFullName" gives you the correct value, already encoded.
Shape each item with "Select". Point a "Select" action at your array of items, switch the map to text mode, and produce one changeset block per item:
--changeset_@{outputs('changesetGuid')}
Content-Type: application/http
Content-Transfer-Encoding: binary
POST https://<insertTenant>.sharepoint.com/sites/<insertSite>/_api/web/lists/getbytitle('Orders')/items HTTP/1.1
Content-Type: application/json;odata=verbose
Accept: application/json;odata=verbose
{"__metadata":{"type":"@{body('Get_entity_type')?['ListItemEntityTypeFullName']}"},"Title":"@{item()?['Title']}","Amount":@{item()?['Amount']}}
Join the blocks into one body with "Compose". Wrap the joined blocks with the outer batch and changeset boundaries using a "Compose" action:
--batch_@{outputs('batchGuid')}
Content-Type: multipart/mixed; boundary="changeset_@{outputs('changesetGuid')}"
@{join(body('Select_map'), decodeUriComponent('%0A'))}
--changeset_@{outputs('changesetGuid')}--
--batch_@{outputs('batchGuid')}--
The "batchGuid" and "changesetGuid" are two "guid" function outputs you generate once at the top of the flow.
Send it. Feed the composed body into a final "Send an HTTP request to SharePoint" action: method POST, Uri "/api/$batch", a "Content-Type" header of "multipart/mixed; boundary=batch@{outputs('batchGuid')}" (the boundary token has to match the "--batch_" delimiters in the body exactly), and an "Accept" of "application/json;odata=verbose". Because you are using the SharePoint action rather than a plain HTTP call, authentication and the form digest are handled for you, so there is no "X-RequestDigest" to fetch.
Keep each batch to about 100 items. The format technically allows up to 1000, but 100 to 250 is the sweet spot that stays clear of timeouts and keeps one malformed item from taking down a huge request. For anything larger, chunk your array and send several batches.
Performance Comparison
The difference between individual calls and batch operations is dramatic:
Individual calls: 600 items means 600 API calls, which hits the rate limit, requires throttling management, and takes several minutes once retry delays kick in.
Batch operations (100 items per batch): 600 items means 6 API calls, which stays far below the rate limit and completes in a fraction of the time with no throttling.
Published benchmarks bear this out. A 1,000-item create that takes several minutes through a standard loop can drop to under two minutes when batched, and equivalent PnP PowerShell batching has taken a 250-second job down to around 25 seconds. The exact multiplier depends on your baseline and item count, but the direction is always the same.
The improvement isn't just about speed. It's about reliability. Flows that use batch operations rarely encounter throttling, which removes a major source of production failures.
Error Handling in Batch Operations
Batch operations have different error handling characteristics than individual operations. If one operation in a batch fails, SharePoint still processes the remaining operations. The response includes a success or failure status for each individual operation inside the batch.
Your flow needs to parse the batch response and check each operation's status code. You can't rely on the batch request itself failing. You must examine the result for each item.
SharePoint batches are not transactional, even inside a changeset. General OData theory treats a changeset as all-or-nothing, but SharePoint and Microsoft 365 explicitly do not roll back. If one write fails, the others still commit, so a partial batch can leave some items created and others missing.
This means you have to implement custom error handling logic to:
- Parse the multipart response
- Extract the status code for each operation
- Identify which items failed
- Retry the failed items individually or in a new batch
In practice, since Power Automate has no multipart parser, you do this with string functions. Split the response on the boundary (for example, split(body('Send_batch'), 'changesetresponse_')), then filter the parts that don't contain a success status like "201 Created". Anything left is a failed operation, and because SharePoint returns results in request order, its position tells you which source item failed. When you need the created data or the error text, slice the JSON out with "indexOf" and "substring" and run it through a "Parse JSON" action. It is fragile string work, so test it against a real response.
Partial failures can be quiet, too. Importing 10,000 items in batches of 100 can land only 7,000 to 9,000 rows with no surfaced error, so treat the per-operation status parsing as mandatory, not optional. The added complexity in error handling is the tradeoff for the performance gains.
Beyond the SharePoint Batch API
The SharePoint /_api/$batch endpoint is the pragmatic choice when your data already lives in a SharePoint list and you're working inside Power Automate. A few alternatives are worth knowing about in 2026:
Microsoft Graph JSON batching (a POST to https://graph.microsoft.com/v1.0/$batch) uses a simpler JSON envelope instead of MIME multipart and works across Microsoft 365 services. It caps each batch at 20 requests and throttles each request individually, but it's the forward-looking option that follows Microsoft's Graph-first direction.
Office Scripts with the "Run script" action can move row-by-row Excel work into a single script call, which avoids per-row connector traffic entirely.
Dataverse bulk operations (the "CreateMultiple" and "UpdateMultiple" messages) are the better fit when the data really belongs in Dataverse rather than a SharePoint list.
Reach for Graph batching when you need to span services or want to future-proof, and stay with the SharePoint batch API when the work is list-centric and already in your flow.
When to Use Batch Operations
Batch operations are worth the implementation complexity when:
You regularly process hundreds or thousands of items in a single flow run. The performance gain justifies the development effort.
You're hitting throttling consistently despite parallelism tuning. Batch operations are the real solution rather than just a workaround.
Reliability is critical. Batch operations give you more predictable performance because throttling becomes rare.
Batch operations may not be worth the effort for flows that:
- Process fewer than 50 items per run
- Run infrequently (a few times per day)
- Have simple error handling requirements
- Need to operate on items independently with complex logic per item
Final Thoughts
Batch operations turn bulk SharePoint work from a throttling nightmare into a reliable, predictable process. Collapsing hundreds of API calls into a handful of requests means flows that used to hit rate limits now stay comfortably below them. The implementation asks more of you: you need to understand the OData batch format, build the HTTP requests by hand, wrap writes in a changeset, and parse multipart responses, including partial failures. That effort pays off when you're processing hundreds or thousands of items regularly, because it fixes throttling at the source instead of just working around it.
Photo by Julian Hochgesang on Unsplash
No comments yet
Be the first to share your thoughts on this article!