5 min readRishi

Speeding Up Apply to Each: Concurrency, Filters, and Select in Power Automate

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.

TechniqueBest forMain caveat
Concurrency controlIndependent per-record workOrder is not guaranteed
Filter arrayIn-memory row reductionStill requires data to be fetched first
SelectMapping arraysNo connector calls inside it
OData filterFetching fewer source rowsRequires correct query syntax
Pagination tuningLarge list or table readsMore data is not always better
Bulk API or batchHigh-volume writesMore 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

Newsletter

New posts, straight to your inbox

One email per post. No spam, no tracking pixels, unsubscribe anytime.

Comments

No comments yet. Be the first.