Wicked Smart Data
LearnInsightsAboutContact
Sign InLet's Build
LearnInsightsAboutContact
Sign InLet's Build
Wicked Smart Data

Intelligence, automation, and expert execution — plus an elite library of free knowledge. We turn complexity into competitive advantage.

Start a conversation

Platform

  • Learning Paths
  • Insights
  • RSS Feed

Company

  • About
  • Contact
  • Work With Us

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Wicked Smart Data. All rights reserved.

Intelligence · Automation · Advantage

All Insights
Power Automate

Implementing Batching and Chunking Strategies in Power Automate: Processing High-Volume Data Sets Efficiently with Do Until Loops, Array Splitting, and API Rate Limit Management

When your Power Automate flow works perfectly on 50 records but collapses under 5,000, you have a batching problem. This expert-level lesson teaches you how to architect production-grade flows using Do Until loops, take/skip array chunking, exponential backoff for API rate limits, and stateful checkpointing so your automation never loses progress—no matter the data volume.

🔥 Expert27 min readSep 22, 2026Updated Sep 22, 2026
Implementing Batching and Chunking Strategies in Power Automate: Processing High-Volume Data Sets Efficiently with Do Until Loops, Array Splitting, and API Rate Limit Management
On this page
  • Introduction
  • Prerequisites
  • Why Batching Is Not Optional for Production Flows
  • Understanding the Array Chunking Problem
  • Building the Do Until Loop Engine
  • Step 1: Initialize Your Variables
  • Step 2: Configure the Do Until Loop
  • Step 3: Extract and Process the Current Chunk
  • Step 4: Increment and Loop
  • A Realistic End-to-End Example: Bulk User Provisioning
  • Handling API Rate Limits: Beyond Simple Delays
  • Reading the 429 Response
  • Implementing Exponential Backoff
  • Advanced Pattern: Stateful Batching with Checkpoint Storage
  • Chunk Size Selection: The Engineering Decision
  • Factor 1: API Rate Limits
  • Factor 2: Action Count Economics
  • Factor 3: Failure Blast Radius
  • Factor 4: Memory and Expression Limits
  • Parallel Batching: When Sequential Isn't Fast Enough
  • Handling Pagination as a Pre-Processing Step
  • Data Transformation Within Chunks
  • Graph API Batch Endpoint: True Server-Side Batching
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • Mistake 1: Forgetting to Increase the Do Until Iteration Limit
  • Mistake 2: Mutating the Source Array During Iteration
  • Mistake 3: Using Apply to Each Loop Index as a Chunk Offset
  • Mistake 4: Not Resetting Per-Item Retry Variables Between Items
  • Mistake 5: Infinite Loop Risk When Exit Condition Is Never Met
  • Mistake 6: Chunk Size Too Large for the JSON Payload Limit
  • Scaling Considerations and Architecture Trade-Offs
  • Summary & Next Steps
  • Implementing Batching and Chunking Strategies in Power Automate: Processing High-Volume Data Sets Efficiently with Do Until Loops, Array Splitting, and API Rate Limit Management

    Introduction

    You've built a flow that works perfectly on your test data — fifty records, clean results, done in thirty seconds. Then you run it against production. Four thousand customer records. Eight hundred SharePoint items. Twelve thousand rows from a SQL export. The flow either times out after its 30-day run limit kicks in unexpectedly, throws a cascade of throttling errors from the downstream API, or burns through your action capacity faster than you thought possible.

    This is the batching problem, and it's one of the most common walls that intermediate Power Automate builders hit when they try to graduate from "clever automation" to "production-grade automation." The gap isn't about knowing more connectors or more expressions. It's about fundamentally rethinking how your flow interacts with data — treating large datasets not as monolithic blobs to be processed all at once, but as streams of work to be divided, paced, and managed deliberately.

    By the end of this lesson, you'll know how to architect flows that can handle hundreds of thousands of records reliably, how to implement array chunking with pure expressions, how to use Do Until loops as your primary engine for bounded batch iteration, and how to build rate-limit-aware flows that respect API throttling constraints without human intervention.

    What you'll learn:

    • How to split large arrays into fixed-size chunks using Power Automate expressions
    • How to implement reliable Do Until loops with proper exit conditions and counter management
    • How to detect and respond to API rate limits (HTTP 429 responses) with exponential backoff
    • How to design a stateful batching architecture that can resume after failures
    • How to select the right chunking strategy based on your API's specific constraints and your data volume

    Prerequisites

    You should be comfortable with Power Automate's expression language — specifically skip(), take(), length(), variables(), and concat(). You should have built at least a few multi-step flows and understand how conditions, loops, and variables work in the designer. Familiarity with HTTP actions and reading API responses is helpful, since much of this lesson involves inspecting status codes and headers. If you haven't worked with the HTTP action and custom connector patterns before, the lesson on advanced custom connectors and HTTP actions will give you the background you need.


    Why Batching Is Not Optional for Production Flows

    Let's be precise about what we mean by "high volume" in the Power Automate context. The platform imposes hard limits that quickly become relevant:

    • Apply to Each concurrency: By default, Apply to Each runs sequentially. You can enable parallel execution, but even then, Power Automate enforces a concurrency cap (typically 50 concurrent iterations) and you'll generate race conditions on shared variables.
    • Flow run duration: A flow can run for a maximum of 30 days, but in practice your organization's environment governance and the underlying Logic Apps infrastructure mean long-running flows are fragile.
    • API call limits: Microsoft 365 connectors and most third-party APIs have rate limits measured in calls per minute or calls per day. A naive "Apply to Each" over 5,000 items will blast through those limits in seconds.
    • Action count licensing: If you're on a per-flow or per-user plan, each action execution counts against your monthly allotment. Processing 10,000 records with 5 actions each means 50,000 action executions — that math matters for your licensing costs.

    Key insight: The single most important mindset shift is recognizing that your flow is not the only consumer of a shared API. SharePoint, Dataverse, Exchange, and every external service you call has throttling that protects all users on the platform. Your flow has a social contract to be a polite consumer.

    The architectural answer is batching: break work into discrete chunks, process one chunk at a time with appropriate pauses, handle failures at the chunk level rather than the item level, and maintain enough state to resume from where you left off if something goes wrong.


    Understanding the Array Chunking Problem

    The foundation of any batching strategy is array chunking — taking a flat array of N items and dividing it into sub-arrays of size K. In languages like Python or JavaScript, this is trivial. In Power Automate's expression language, it requires more thought because the platform doesn't have a native chunk() function.

    The strategy we'll use relies on three functions working together:

    • skip(array, count) — returns everything in the array after skipping the first count items
    • take(array, count) — returns the first count items from an array
    • length(array) — returns the number of items in an array

    To get chunk n (zero-indexed) of size K from an array called allItems, the expression is:

    take(skip(variables('allItems'), mul(variables('chunkIndex'), variables('chunkSize'))), variables('chunkSize'))
    

    Let's break that down:

    • mul(variables('chunkIndex'), variables('chunkSize')) calculates the starting offset (e.g., chunk 3 of size 100 starts at position 300)
    • skip(variables('allItems'), <offset>) removes everything before that position
    • take(<result>, variables('chunkSize')) takes only the next K items

    When you're on the last chunk, take() will naturally return fewer than K items without error — it just returns whatever remains. That's exactly the behavior you want.

    Tip: Always compute your total chunk count upfront and store it in a variable. Use div(length(variables('allItems')), variables('chunkSize')) for the base, then add 1 if there's a remainder (mod(length(variables('allItems')), variables('chunkSize')) is greater than 0). You'll need this to set your Do Until exit condition correctly.


    Building the Do Until Loop Engine

    The Do Until loop is Power Automate's mechanism for repeating a block of actions until a condition is met. It's more flexible than Apply to Each for batching because you control the iteration explicitly — you decide when to advance, when to pause, and when to stop.

    Here's the architectural skeleton for a complete batching engine:

    Step 1: Initialize Your Variables

    Before the Do Until loop, initialize these variables:

    Variable Type Initial Value Purpose
    allItems Array (your dataset) The full dataset to process
    chunkIndex Integer 0 Current chunk (zero-indexed)
    chunkSize Integer 100 How many items per batch
    totalChunks Integer (calculated) Total number of batches
    processingErrors Array [] Error log for failed items
    isComplete Boolean false Loop exit flag

    For totalChunks, use a Compose action with this expression:

    add(
      div(length(variables('allItems')), variables('chunkSize')),
      if(
        greater(mod(length(variables('allItems')), variables('chunkSize')), 0),
        1,
        0
      )
    )
    

    Set your totalChunks variable with the output of that Compose.

    Step 2: Configure the Do Until Loop

    Add a Do Until loop with this exit condition:

    greaterOrEquals(variables('chunkIndex'), variables('totalChunks'))
    

    Set the loop limit to something generous — if you're processing 10,000 items in chunks of 100, you need 100 iterations. The default Do Until limit is 60 iterations, which you can increase up to 5,000 in the loop settings. Set the timeout to something appropriate for your data volume, such as PT12H (12 hours in ISO 8601 duration format).

    Warning: The "Count" limit on Do Until loops is often overlooked. If your loop exits prematurely because you hit the iteration limit before processing all chunks, you'll get a silently incomplete run. Always calculate your maximum needed iterations and set the limit accordingly — never leave it at the default 60.

    Step 3: Extract and Process the Current Chunk

    Inside the Do Until, add a Compose action to extract the current chunk:

    take(
      skip(variables('allItems'), mul(variables('chunkIndex'), variables('chunkSize'))),
      variables('chunkSize')
    )
    

    This output is your currentChunk — an array of up to chunkSize items. Now you process this chunk with an Apply to Each over outputs('Compose_CurrentChunk').

    Step 4: Increment and Loop

    At the end of the Do Until body (outside the Apply to Each), increment your counter:

    Set variable: chunkIndex = add(variables('chunkIndex'), 1)
    

    This is the heartbeat of your engine. Every iteration of the Do Until processes one chunk and advances the pointer.


    A Realistic End-to-End Example: Bulk User Provisioning

    Let's make this concrete. Imagine you're automating user provisioning: you receive a CSV file with 3,000 new employees from HR, and you need to create each one in Azure AD and then send them a welcome email. The Microsoft Graph API has a per-user rate limit, and the email connector has its own throttling. You can't just loop over 3,000 items.

    Your flow architecture looks like this:

    Trigger: Recurrence (daily at 6 AM) or HTTP trigger from an upstream system

    Phase 1 — Data Loading:

    1. Get the CSV file from SharePoint or OneDrive
    2. Parse the CSV content into an array (using the Parse JSON action or a series of string splits)
    3. Store the parsed array in allItems
    4. Calculate and store totalChunks

    Phase 2 — Batch Processing (Do Until loop):

    Do Until: chunkIndex >= totalChunks
      |
      ├── Compose: Extract current chunk (take/skip expression)
      |
      ├── Apply to Each: currentChunk
      |   ├── HTTP: POST to Graph API /users endpoint
      |   ├── Condition: Check response status
      |   |   ├── 201 Created → Compose success log entry
      |   |   ├── 429 Too Many Requests → Delay + Retry (see next section)
      |   |   └── Other error → Append to processingErrors
      |   └── Append to processingErrors (if needed)
      |
      ├── Delay: 2 seconds (inter-chunk pause)
      |
      └── Set variable: chunkIndex = chunkIndex + 1
    

    Phase 3 — Completion:

    1. Condition: Is processingErrors empty?
    2. Yes → Send success summary email
    3. No → Send failure report with error details

    Notice the deliberate 2-second delay between chunks. This is your primary throttle control mechanism. With a chunk size of 50 and a 2-second inter-chunk pause, you're processing 50 items roughly every 2 seconds — around 25 items per second. If the Graph API limit is 300 requests per minute for your tenant, you're safely under that ceiling.

    Note: The right chunk size and inter-chunk delay are specific to your target API and your data characteristics. Start conservative (small chunks, longer pauses), observe your flow run times in Run History, and tune from there. Don't optimize prematurely.


    Handling API Rate Limits: Beyond Simple Delays

    A fixed inter-chunk delay is a blunt instrument. It works, but it's inefficient — if the API is lightly loaded, you're waiting unnecessarily. If the API is heavily throttled, a fixed delay might not be enough. The more sophisticated approach is reactive rate limiting: detect when the API is telling you to slow down, and respond accordingly.

    Reading the 429 Response

    When a well-behaved API is being throttled, it returns HTTP 429 Too Many Requests with a Retry-After header. This header tells you exactly how many seconds to wait before retrying. Power Automate can read this:

    In your HTTP action that calls the API, configure it to run whether the previous action succeeded or failed (configure the Run After settings). Then add a Condition immediately after:

    Condition: outputs('HTTP_CreateUser')['statusCode'] is equal to 429

    If true:

    1. Add a Compose action: outputs('HTTP_CreateUser')['headers']['Retry-After']
    2. Add a Delay action: Duration = outputs('Compose_RetryAfter') seconds
    3. Re-issue the same HTTP call

    This creates a mini retry loop within your main batching loop.

    Implementing Exponential Backoff

    For APIs that don't return a Retry-After header, or for network errors (5xx responses), exponential backoff is the industry standard approach. The idea: your first retry waits 1 second, the second waits 2 seconds, the fourth waits 8 seconds, and so on. This prevents a thundering herd of retries from making the situation worse.

    Implement this with a nested Do Until inside your main batch loop:

    Initialize: retryCount = 0
    Initialize: retryDelay = 1 (second)
    Initialize: apiCallSucceeded = false
    
    Do Until: apiCallSucceeded OR retryCount >= 5
      |
      ├── HTTP: Call the API
      |
      ├── Condition: Status code is 200 or 201?
      |   ├── Yes → Set apiCallSucceeded = true
      |   └── No:
      |       ├── Set retryDelay = mul(variables('retryDelay'), 2)
      |       ├── Set retryCount = add(variables('retryCount'), 1)
      |       └── Delay: retryDelay seconds
    

    The delay sequence becomes 1, 2, 4, 8, 16 seconds before giving up. After 5 attempts with no success, you append the failed item to your processingErrors array and move on — you don't want one stubborn API call to block your entire batch.

    Tip: Add jitter to your exponential backoff by adding a small random offset to the delay. In distributed systems, pure exponential backoff can cause synchronized retry storms when multiple flow instances are running simultaneously. In Power Automate, use rand(1, 5) to add 1-4 random seconds to each retry delay.

    For a deeper dive into structured retry patterns across all your flows, the article on error handling and retry patterns covers this topic comprehensively including scope-level error trapping.


    Advanced Pattern: Stateful Batching with Checkpoint Storage

    So far, our batching engine keeps all state in variables — which means if the flow fails mid-run, the state is lost and you restart from scratch. For truly critical high-volume processing, you need a stateful design: the flow saves its progress periodically, and if it fails or is terminated, the next run picks up from the checkpoint.

    The checkpoint mechanism requires external storage. The most practical options in Power Automate are:

    • SharePoint List: Simple, no additional license needed, supports concurrent reads
    • Dataverse Table: More robust, better querying, requires Dataverse environment
    • Azure Blob Storage: Best for large state payloads, requires Azure subscription

    Here's how the pattern works with SharePoint:

    Checkpoint Schema (SharePoint List):

    Column Type Description
    RunId Text Unique identifier for this processing job
    LastProcessedChunk Number Index of the last successfully completed chunk
    TotalChunks Number Total chunks in this job
    Status Choice Pending / In Progress / Complete / Failed
    ErrorCount Number Running count of item-level errors
    StartedAt DateTime When this job began

    Modified Flow Architecture:

    On start:

    1. Check if a checkpoint exists for the current job (query SharePoint by RunId)
    2. If yes: load LastProcessedChunk and set chunkIndex = LastProcessedChunk + 1
    3. If no: create a new checkpoint record, set chunkIndex = 0

    Inside the Do Until loop, after each chunk completes:

    1. Update the SharePoint checkpoint record: LastProcessedChunk = chunkIndex
    2. Increment chunkIndex

    On completion:

    1. Update checkpoint status to "Complete"

    If the flow fails at chunk 47 of 200, the next run sees the checkpoint, skips chunks 0-47, and resumes from chunk 48. No reprocessing, no data loss.

    Key insight: Checkpoint storage also gives you an audit trail for free. After the flow runs, you have a SharePoint record showing exactly how long each batch job took, how many errors occurred, and whether it completed successfully. This data is invaluable for capacity planning and troubleshooting.

    This pattern pairs naturally with orchestrating child flows — the checkpoint management can live in a parent orchestrator flow, while each chunk is processed by a child flow that receives the chunk as its input parameter.


    Chunk Size Selection: The Engineering Decision

    Choosing the right chunk size is more nuanced than "just use 100." It's an engineering decision that depends on several factors:

    Factor 1: API Rate Limits

    Most APIs express limits as "X requests per Y seconds." Divide X by Y to get your per-second budget. With a chunk size of K and an inter-chunk delay of D seconds, your sustained request rate is K / (processing_time + D). Make sure this rate stays below the API's per-second limit.

    For the Microsoft Graph API on an enterprise tenant, the limit is typically 300 requests per second per application, but the practical throttling kicks in much earlier for bulk operations. Starting with chunks of 25-50 and a 1-second delay is a safe baseline.

    Factor 2: Action Count Economics

    Remember that every action inside your Do Until loop runs once per chunk. Every action inside your Apply to Each runs once per item. With 1,000 items in chunks of 100:

    • Do Until iterations: 10
    • Apply to Each iterations: 1,000

    If your Apply to Each has 5 actions, that's 5,000 action executions from the inner loop alone, plus 10 × (however many actions are in your chunk-level logic). Larger chunks mean more Apply to Each executions relative to Do Until overhead — but also larger payloads if you're processing the chunk as a unit.

    For licensing awareness, review Power Automate licensing options to understand how action limits apply to your specific plan.

    Factor 3: Failure Blast Radius

    Smaller chunks mean that when something fails, you lose less work. If a network error kills the flow during Apply to Each over a chunk of 500, you need to reprocess 500 items. With chunks of 50, the maximum rework is 50 items. The checkpoint pattern makes this precise: you know exactly which chunk failed.

    Factor 4: Memory and Expression Limits

    Power Automate has undocumented (and variable) memory limits on how large an expression can be and how large a variable payload can be stored. Arrays with thousands of complex JSON objects can hit these limits. If you're loading a full dataset into a single variable, test with your real data before committing to a chunk size.

    Practical Starting Points:

    Scenario Recommended Chunk Size Inter-Chunk Delay
    Microsoft Graph API (users/groups) 25-50 2 seconds
    SharePoint list operations 100-500 1 second
    External REST APIs (conservative) 10-25 5 seconds
    Internal/high-limit APIs 100-500 0-1 second
    Email sending (Exchange) 10-20 5 seconds

    Parallel Batching: When Sequential Isn't Fast Enough

    Sequential batch processing is safe and predictable, but it's slow. If you have a time-sensitive job and your API can support higher throughput, you can parallelize at the chunk level.

    The architecture: instead of processing one chunk at a time in a Do Until loop, you pre-generate all chunks, store them in a SharePoint list or Azure Service Bus queue, and then fan out to multiple child flows that each claim and process one chunk.

    The Queue-Based Approach:

    1. Parent flow: generate all chunks, enqueue each one as a separate message in Azure Service Bus
    2. Worker flows (triggered by Service Bus messages): each worker dequeues one message, processes the chunk, marks the message complete
    3. If a worker fails, Service Bus automatically re-enqueues the message for another worker to pick up

    This gives you inherent fault tolerance (failed chunks are automatically retried) and horizontal scaling (add more worker flow instances to increase throughput).

    The tradeoff: more architectural complexity, requires Azure Service Bus, and you need to be careful about concurrent writes to shared state (like a results store). The lesson on parallel branching and concurrency control covers the concurrency hazards you'll encounter in parallel designs.

    For smaller-scale parallel batching (without Azure Service Bus), you can use Power Automate's "Run a Child Flow" action with multiple parallel branches in your parent flow, each branch processing a different chunk. This works well for up to 10-20 parallel executions before the concurrency overhead dominates.


    Handling Pagination as a Pre-Processing Step

    A closely related challenge: what if your data source paginates and you don't have the full dataset loaded before you start batching? This is common with SharePoint lists (4,000-item view threshold), REST APIs that return 100 items per page, and SQL queries with result set limits.

    The solution is a two-phase approach:

    Phase 1 — Full Data Collection: Use a separate Do Until loop to collect all pages from the data source into your allItems array before any processing begins.

    Initialize: allItems = []
    Initialize: nextPageToken = ""
    Initialize: hasMorePages = true
    
    Do Until: hasMorePages = false
      |
      ├── HTTP: GET /api/items?pageToken=nextPageToken&pageSize=100
      |
      ├── Apply to Each: response body items
      |   └── Append to Array: allItems
      |
      ├── Condition: Does response contain nextPageToken?
      |   ├── Yes → Set nextPageToken = response.nextPageToken
      |   └── No → Set hasMorePages = false
    

    Phase 2 — Batch Processing: Only after Phase 1 completes and allItems contains everything do you start your batching engine from the previous sections.

    Warning: Storing tens of thousands of JSON objects in a single Power Automate array variable can cause performance degradation or hit undocumented payload limits. For truly massive datasets (50,000+ records), consider an intermediate storage strategy: write each page to SharePoint or Dataverse during collection, then read from there during processing rather than keeping everything in memory.

    For a focused look at the pagination side of this problem, the lesson on handling pagination and throttling when querying large datasets goes deep on the mechanics of different API pagination styles (cursor-based, offset-based, token-based) and how to handle each in Power Automate.


    Data Transformation Within Chunks

    Processing high-volume data often involves transforming records before sending them to the target system. The Select and Filter Array actions in Power Automate are purpose-built for bulk transformation and filtering — and critically, they operate on an entire array without looping, which is much more efficient than using Apply to Each for transformations.

    For your current chunk, you can chain transformations before the API call:

    Compose (currentChunk) 
      → Filter Array (exclude inactive records)
      → Select (reshape each record to API's expected format)
      → HTTP (send transformed batch to API)
    

    The Select action is particularly powerful. For a chunk of user records that need to be reshaped for a Graph API batch request, your Select map might look like:

    {
      "accountEnabled": true,
      "displayName": "@{item()['FullName']}",
      "mailNickname": "@{toLower(first(split(item()['Email'], '@')))}",
      "userPrincipalName": "@{item()['Email']}",
      "passwordProfile": {
        "forceChangePasswordNextSignIn": true,
        "password": "TempPass2024!"
      }
    }
    

    This runs once per item within the Select action itself — not as a separate Apply to Each iteration — making it significantly more efficient. For comprehensive coverage of Select, Filter, and Compose together, the article on transforming and mapping data with Compose, Select, and Filter Array is essential reading.


    Graph API Batch Endpoint: True Server-Side Batching

    If you're working with Microsoft Graph API specifically, there's a much more powerful option than making individual API calls for each item: the Graph API's $batch endpoint lets you send up to 20 requests in a single HTTP call. This is true server-side batching and can dramatically reduce the number of HTTP actions your flow executes.

    The batch request format:

    {
      "requests": [
        {
          "id": "1",
          "method": "POST",
          "url": "/users",
          "headers": { "Content-Type": "application/json" },
          "body": { ... user 1 ... }
        },
        {
          "id": "2", 
          "method": "POST",
          "url": "/users",
          "headers": { "Content-Type": "application/json" },
          "body": { ... user 2 ... }
        }
      ]
    }
    

    To use this with your batching engine:

    1. Set your chunkSize to 20 (the batch endpoint maximum)
    2. Inside the Do Until, use a Select action to reshape your current chunk into the requests array format above
    3. Send one HTTP POST to https://graph.microsoft.com/v1.0/$batch
    4. Parse the batch response to check individual request statuses

    The batch response contains a responses array with a status code for each sub-request. You process these exactly as you'd handle individual responses — check each status code, retry 429s, log failures.

    This approach reduces a 1,000-item operation from 1,000 HTTP calls to 50 batch HTTP calls. The action count reduction and speed improvement are substantial.


    Hands-On Exercise

    Build a complete batching flow that processes a large dataset with rate limit handling. Use the following scenario: you have a SharePoint list called "CustomerEmails" with 500 items, each containing a customer email address and a personalized message. You need to send each customer their message via the Outlook connector, with a maximum of 10 emails per minute (a realistic Exchange throttling constraint).

    Step 1: Set Up Test Data Create a SharePoint list with at least 50 items (testing with all 500 is optional but instructive). Each item should have CustomerEmail (text) and PersonalizedMessage (multiline text) columns.

    Step 2: Initialize the Flow

    Create a manually triggered flow with these variables:

    • allItems (Array, initially [])
    • chunkIndex (Integer, 0)
    • chunkSize (Integer, 10)
    • totalChunks (Integer, 0)
    • emailErrors (Array, [])

    Add a "Get items" action for your SharePoint list (enable pagination in settings if needed, set threshold to 500). Use an Apply to Each to populate allItems from the SharePoint response — append each item's body to the array.

    Step 3: Calculate Total Chunks

    Add a Compose action with the expression:

    add(
      div(length(variables('allItems')), variables('chunkSize')),
      if(greater(mod(length(variables('allItems')), variables('chunkSize')), 0), 1, 0)
    )
    

    Set totalChunks from this output.

    Step 4: Build the Do Until Loop

    Configure: chunkIndex >= totalChunks, limit 100 iterations, timeout PT2H.

    Inside the loop:

    1. Compose the current chunk using the take/skip expression
    2. Apply to Each over the current chunk:
      • Send an email using the Outlook connector: To = items('Apply_to_each')['CustomerEmail'], Body = items('Apply_to_each')['PersonalizedMessage']
      • Add a Condition checking the email action's status — if it returns an error, append the item to emailErrors
    3. Add a Delay action: 60 seconds (to stay under 10/minute limit)
    4. Increment chunkIndex

    Step 5: Completion Logic

    After the Do Until:

    1. Condition: Is length of emailErrors equal to 0?
    2. Yes: Send yourself a summary email: "Batch complete. 500 emails sent successfully."
    3. No: Send an error report: "Batch complete with errors. length(variables('emailErrors')) emails failed." Attach the error array as JSON in the body.

    Challenge extension: Modify the flow to read and write a checkpoint to a second SharePoint list so that if the flow is cancelled at chunk 7, the next manual run resumes from chunk 8 instead of restarting.


    Common Mistakes & Troubleshooting

    Mistake 1: Forgetting to Increase the Do Until Iteration Limit

    Symptom: Your flow completes successfully but only processes some of your data. The run history shows the Do Until loop exited cleanly, but chunkIndex is less than totalChunks.

    Cause: The default iteration limit of 60 was hit. The loop exits successfully (not as an error) when the limit is reached, which is why this is so insidious.

    Fix: In the Do Until settings, set "Count" to at least your maximum expected chunk count. For safety, set it generously — if you might have 200 chunks, set the limit to 300.

    Mistake 2: Mutating the Source Array During Iteration

    Symptom: Your chunk boundaries drift over time. Records get processed twice or are skipped entirely.

    Cause: Something inside your loop is modifying allItems — perhaps an "Append to array variable" that's accidentally targeting the source array instead of a results array.

    Fix: Treat allItems as immutable once the loop starts. Never write to it inside the Do Until. Use separate variables for results and errors.

    Mistake 3: Using Apply to Each Loop Index as a Chunk Offset

    Symptom: Works in testing, fails silently with parallel Apply to Each enabled.

    Cause: Some flows try to use the implicit index within Apply to Each for chunking logic. When parallel execution is enabled, iteration order is non-deterministic and the index becomes meaningless.

    Fix: Use the explicit chunkIndex variable and take/skip expressions. These are deterministic regardless of Apply to Each concurrency settings.

    Mistake 4: Not Resetting Per-Item Retry Variables Between Items

    Symptom: After a successful item, the next item's retry delay starts at 16 seconds instead of 1 second.

    Cause: retryDelay and retryCount variables from the exponential backoff pattern aren't reset at the start of each Apply to Each iteration.

    Fix: Add "Set variable" actions at the beginning of each Apply to Each iteration to reset retryCount = 0 and retryDelay = 1 before each item's processing block.

    Mistake 5: Infinite Loop Risk When Exit Condition Is Never Met

    Symptom: Flow runs until it hits the iteration limit or timeout.

    Cause: A logic error in chunkIndex incrementing — perhaps the increment action is inside a conditional branch that doesn't always execute.

    Fix: Place the chunkIndex increment unconditionally at the very end of the Do Until body, outside all conditions and error handlers. The counter must advance on every iteration, regardless of what happened during processing.

    Warning: Always test your Do Until exit condition with a small dataset where you can manually verify the final chunkIndex value matches totalChunks in the run history. An infinite loop on production data is expensive both in action count and in potential rate limit damage to your tenant.

    Mistake 6: Chunk Size Too Large for the JSON Payload Limit

    Symptom: Compose actions silently truncate large arrays, or flow throws "The request body exceeds the maximum size" errors.

    Cause: Power Automate has a 100MB limit on action inputs/outputs (though the practical limit for in-memory variables is much lower, around 5-10MB for complex JSON).

    Fix: If your items are large JSON objects (e.g., rich SharePoint list items with many columns), reduce chunk size significantly. Test with length(outputs('Compose_CurrentChunk')) to verify you're getting the expected count.


    Scaling Considerations and Architecture Trade-Offs

    As your data volumes grow, the pure Power Automate batching approach will eventually hit practical limits. Here's how to think about when to augment or replace parts of the pattern:

    Under 50,000 items: The Do Until + take/skip pattern works well with appropriate chunk sizes and delays.

    50,000 to 500,000 items: Consider the queue-based parallel pattern with Azure Service Bus. Also consider whether Power Automate is the right tool for the heavy lifting — Azure Data Factory, Azure Functions, or Logic Apps with their higher action limits may be more appropriate for the processing layer, with Power Automate handling orchestration and notification.

    Over 500,000 items: Power Automate should probably be your orchestrator only, not your processor. Use it to trigger a Synapse Analytics pipeline, Azure Batch job, or Databricks notebook. Power Automate is excellent at event-driven trigger logic and business process integration; raw data processing at scale is better served by purpose-built data platforms.

    For enterprise-scale deployments, the way you structure and deploy these flows matters as much as the flow logic itself. The article on deploying and managing Power Automate solutions across environments covers how to parameterize your chunk sizes and API endpoints via environment variables so the same flow works in dev, test, and production without code changes.


    Summary & Next Steps

    You've now got the full toolkit for high-volume data processing in Power Automate. Let's consolidate what you've built:

    The core pattern is a Do Until loop using take/skip expressions to extract fixed-size chunks from a pre-loaded array. This gives you explicit control over iteration boundaries, failure scope, and progress tracking.

    Rate limit management requires a two-layer approach: a proactive layer (fixed inter-chunk delays sized to stay under API limits) and a reactive layer (HTTP 429 detection with Retry-After header parsing or exponential backoff). Neither alone is sufficient for production robustness.

    Stateful checkpointing turns a fragile single-run operation into a resumable job. The extra complexity of writing checkpoint data to SharePoint or Dataverse pays for itself the first time your flow fails at record 4,723 of 5,000 and you don't have to start over.

    The right chunk size is an engineering calculation, not a guess. Start from your API's rate limits, work backward to a safe sustained request rate, then choose a chunk size and delay combination that achieves that rate.

    Graph API batching ($batch endpoint) is the elite move for Microsoft 365 scenarios — it compresses 20 API calls into 1 HTTP action, which is both faster and massively more action-efficient.

    Where to go next:

    • If your batching flows are calling external APIs you don't directly control, deepen your HTTP action expertise with Advanced Power Automate: Custom Connectors and HTTP Actions for Production Integration
    • If you want to move toward event-driven, queue-based architecture for truly high-scale workloads, Implementing Event-Driven Automation with Power Automate and Azure Service Bus is the natural continuation
    • If your batches involve complex error handling at the scope level rather than the item level, Master Error Handling and Retry Patterns in Power Automate will fill the remaining gaps

    The ability to process large datasets reliably and efficiently separates automation that runs in demos from automation that runs in production. You now have the architecture to build the latter.

    Work With Us

    From insight to implementation

    Reading is the start. When you're ready to build the data, automation, or AI systems behind it, our team turns strategy into shipped results.

    Let's Build

    Flow Automation Basics

    Previous

    Automating SharePoint Document Library Version Control with Power Automate: Archiving, Restoring, and Enforcing Retention Policies Across Sites

    Related Insights

    Power AutomatePractitioner

    Automating SharePoint Document Library Version Control with Power Automate: Archiving, Restoring, and Enforcing Retention Policies Across Sites

    23 min
    Power AutomateFoundation

    Automating Microsoft 365 User Onboarding with Power Automate: Provisioning Accounts, Assigning Licenses, and Sending Welcome Emails from a Single Flow

    17 min
    Power AutomateExpert

    Building a Power Automate Monitoring and Alerting System: Detecting Failed Flows, Notifying Owners, and Logging Telemetry to Azure Application Insights

    30 min

    On this page

    • Introduction
    • Prerequisites
    • Why Batching Is Not Optional for Production Flows
    • Understanding the Array Chunking Problem
    • Building the Do Until Loop Engine
    • Step 1: Initialize Your Variables
    • Step 2: Configure the Do Until Loop
    • Step 3: Extract and Process the Current Chunk
    • Step 4: Increment and Loop
    • A Realistic End-to-End Example: Bulk User Provisioning
    • Handling API Rate Limits: Beyond Simple Delays
    • Reading the 429 Response
    • Implementing Exponential Backoff
    • Advanced Pattern: Stateful Batching with Checkpoint Storage
    • Chunk Size Selection: The Engineering Decision
    • Factor 1: API Rate Limits
    • Factor 2: Action Count Economics
    • Factor 3: Failure Blast Radius
    • Factor 4: Memory and Expression Limits
    • Parallel Batching: When Sequential Isn't Fast Enough
    • Handling Pagination as a Pre-Processing Step
    • Data Transformation Within Chunks
    • Graph API Batch Endpoint: True Server-Side Batching
    • Hands-On Exercise
    • Common Mistakes & Troubleshooting
    • Mistake 1: Forgetting to Increase the Do Until Iteration Limit
    • Mistake 2: Mutating the Source Array During Iteration
    • Mistake 3: Using Apply to Each Loop Index as a Chunk Offset
    • Mistake 4: Not Resetting Per-Item Retry Variables Between Items
    • Mistake 5: Infinite Loop Risk When Exit Condition Is Never Met
    • Mistake 6: Chunk Size Too Large for the JSON Payload Limit
    • Scaling Considerations and Architecture Trade-Offs
    • Summary & Next Steps