Speeding Up Apply to Each: Concurrency, Filters, and Select in Power Automate
In this series: Power Automate gotchas
Apply to each is where many good flows become slow flows. The designer makes it easy to add actions inside a loop, and every extra action multiplies by every record. If the flow touches hundreds or thousands of rows, performance is decided before the first connector call runs.
The fastest loop is the loop you delete
Start by challenging the loop. Many loops exist only to reshape data, remove rows, or build a list of strings. Those jobs often belong to Data Operations actions like Select, Filter array, and Join. They run as single actions over an array and avoid the overhead of per-item actions.
Count actions, not just records. A loop with four actions over five thousand records is twenty thousand action executions. That increases runtime, cost pressure, throttling risk, and failure surface. A Select followed by one bulk-friendly action is almost always easier to support.
| Technique | Best for | Main caveat |
|---|---|---|
| Concurrency control | Independent per-record work | Order is not guaranteed |
| Filter array | In-memory row reduction | Still requires data to be fetched first |
| Select | Mapping arrays | No connector calls inside it |
| OData filter | Fetching fewer source rows | Requires correct query syntax |
| Pagination tuning | Large list or table reads | More data is not always better |
| Bulk API or batch | High-volume writes | More complex error handling |
Concurrency helps only when iterations are independent
Turn on concurrency deliberately. In the Apply to each settings, enable concurrency control and choose a degree of parallelism. For independent HTTP calls, notification sends, or row updates, parallelism can cut runtime dramatically. Start with a conservative value and increase only after watching throttling and target-system behavior.
Before:
Apply to each account
Get related contacts
Send notification
Update account
After:
Apply to each account with concurrency enabled
Get related contacts
Send notification
Update account
Do not use shared variables in concurrent loops. Appending to array variables, incrementing counters, and building strings inside a parallel loop can produce unpredictable order and race-like behavior. If the output needs ordering, build it with Select outside the loop or sort downstream.
Filter before the loop, preferably at the source
Use OData when the connector supports it. Filtering at the source means the connector retrieves fewer rows and the loop has less work to do. In Dataverse and SharePoint, this is often the single biggest improvement. Fetch active records, recent records, or records missing a required value rather than fetching everything and deciding later.
Dataverse List rows filter:
statecode eq 0 and createdon ge 2026-05-01T00:00:00Z
SharePoint Get items filter:
Status eq 'Ready' and Priority eq 'High'
Use Filter array when the source cannot do it. Sometimes the source connector has weak filtering, or the condition depends on computed data. Filter array is still better than entering a loop and placing a Condition inside it, because it separates selection from processing.
Before:
Apply to each item
Condition status is Ready
Update item
After:
Filter array where status is Ready
Apply to each filtered item
Update item
Select is the workhorse for reshaping arrays
Map once, use many times. Select can transform an array of objects into the exact shape needed for email tables, CSV rows, batch payload fragments, or a smaller internal contract. Instead of appending to an array variable in a loop, use Select to produce the array in one action.
{
"accountId": "@item()?['accountid']",
"name": "@item()?['name']",
"ownerEmail": "@item()?['_ownerid_value@OData.Community.Display.V1.FormattedValue']"
}
Pair Select with Join when the output is text. If you need semicolon-separated email addresses or a simple line list, Select the field first, then Join the array. That is two actions regardless of whether you have ten records or ten thousand.
Pagination can save or sink the run
Understand the one hundred thousand angle. Many list actions can paginate beyond their default limits, and some support thresholds up to one hundred thousand records. Turning that on without filtering is not optimization. It simply gives your loop more work and makes failures more expensive.
Set pagination based on the real batch size. If the business process only needs today’s ready records, use an OData filter and a realistic threshold. If you truly need a large export, design for chunking, logging, and restartability. A giant Apply to each with no checkpoint is a recovery problem waiting to happen.
Connector calls inside loops deserve suspicion
Move lookups out when possible. If every iteration gets the same configuration row, retrieve it once before the loop. If every iteration checks the same team membership, cache the data in an array and filter in memory. Repeated identical connector calls are one of the easiest performance bugs to miss in run history.
Prefer bulk-friendly targets. Some systems offer batch endpoints or import APIs. Dataverse Web API batch, SharePoint batch patterns, and purpose-built integration endpoints can reduce thousands of writes into fewer network round trips. That raises the bar for error handling, but it is the right trade when volume is real.
Measure with run history before celebrating
Compare action counts and elapsed time. A faster-looking flow is not enough. Capture the number of source records, number of loop iterations, action count, and total duration before and after. Also check failure behavior. A concurrent loop that fails faster but leaves inconsistent updates is not an improvement.
Apply to each is not bad. Unexamined Apply to each is bad. Filter earlier, reshape with Select, use concurrency only for independent work, and treat every action inside the loop as a multiplier you must justify.
Keep reading
Filter Rows: The Lookup GUID Is _parentcustomerid_value
Filter rows is Dataverse OData. Lookups filter on _logicalname_value, not the name. Filter array runs after the download, and @equals does not belong in that box.
Get a Row by ID: Missing Fails the Run, Empty List Rows Does Not
Get a row by ID fails the run when the GUID is missing. List rows with zero matches succeeds. Look up the business key and branch on empty.
When an HTTP Request Is Received: The Callback URL Is a Secret
The HTTP trigger URL carries a SAS signature. Anyone with it can start the flow. Restrict who can call, always Respond, and never treat that URL as public.
Power Automate Do Until: The Loop That Succeeds After Giving Up
A Do until that hits count or timeout still succeeds. Put Get inside the loop, add a Delay, and test the condition after the loop exits.
Power Automate Trigger Conditions: The Run That Never Appears
A trigger condition that is false does not start a run and does not leave run history. How the expression differs from a Condition action, and how to see the skipped checks.
Power Platform September 2026 Update: The App-Builder Skill, Agent Plugins, and the Refreshed Model-Driven UI
What matters in Microsoft's September 2026 Power Platform feature update: the model app-builder skill goes GA, the canvas authoring agent plugin goes GA, generative pages embed in forms and reach connectors, model-driven header refresh is GA with display density in preview, and Power Automate gets server-side search.
Newsletter
New posts, straight to your inbox
One email when a new post goes up. Double opt-in, unsubscribe anytime. Not a daily digest.
Comments
- No comments yet. Be the first.