You've built a Power Automate flow that processes employee onboarding records. It creates an Azure AD account, provisions a SharePoint site, sends a welcome email, assigns licenses, and logs everything to a SQL database. The flow runs perfectly — but it takes four and a half minutes per record, and HR needs to process 200 new hires every Monday morning. You do the math: 900 minutes, serialized. That's 15 hours of processing time for a batch that needs to finish before the workday starts.
The problem isn't the logic. The problem is that you're running independent operations sequentially when they could run simultaneously. Power Automate supports genuine parallel execution through parallel branches, and when combined with thoughtful concurrency control on loops and shared resources, you can compress that 15-hour job into something that finishes during the team standup. But concurrency without discipline introduces a new class of problems — race conditions, duplicate writes, throttling collisions, and partial failures that are nightmarish to debug — so you can't just "add parallelism" and call it done.
By the end of this lesson, you'll understand how Power Automate implements concurrency at the engine level, how to architect flows that exploit parallelism safely, and how to design around the real-world failure modes that bite every developer who reaches for these features without fully understanding them.
What you'll learn:
runAfter conditions to control branch dependenciesApply to each loops and understand the trade-offs between throughput and API throttle limitsThis lesson assumes you're past the beginner stage. You should already be comfortable with:
compose actions, and the expression editorApply to each loops work, including their default serialized behaviorconfigure run after, scope actions)If you haven't worked with runAfter conditions or scope-based error handling yet, spend 30 minutes with those concepts first — they underpin everything in this lesson.
Before you configure a single parallel branch, you need an accurate mental model of what Power Automate is actually doing under the hood. Most documentation glosses over this, and the gap between the simplified explanation and reality is exactly where bugs live.
Power Automate is built on top of Azure Logic Apps' workflow execution engine. When your flow runs, the engine serializes the entire workflow definition as a JSON document (the workflow definition language, or WDL) and then dispatches action execution to a distributed worker pool. Each action is essentially a stateless task that the engine schedules, executes, and records the result of.
When you create a parallel branch in the designer, what you're doing in the underlying WDL is defining two or more actions that share the same runAfter dependency — meaning both actions declare "run after action X succeeds" rather than one running after the other. The engine sees these sibling dependencies and dispatches both actions to the worker pool simultaneously.
Here's the critical nuance: "parallel" in Power Automate means the engine dispatches actions concurrently, but it does not guarantee simultaneous start times. Worker availability, connector throttling, and queue depth all introduce jitter. Two branches dispatched "at the same time" might start 200ms apart or 2 seconds apart. For most business logic, this doesn't matter. For race conditions involving shared writes, it matters enormously.
The other thing to understand is that parallel branches must converge before the flow continues. If you have three parallel branches after step 2, step 3 (whatever comes after all three branches) cannot start until all three branches complete. This is the fan-out/fan-in pattern, and it's how Power Automate enforces sequential integrity on the flow's critical path even when individual branches run concurrently.
Important: Power Automate has a limit on how many actions can run concurrently within a single flow run. For most plans, the practical limit is around 50 concurrent branches, though this depends on your subscription tier and the connectors involved. Don't plan an architecture around 500 concurrent branches — it won't work the way you expect.
Let's work with a concrete scenario throughout this lesson: processing a batch of sales orders. Each order needs to:
Operations 2, 3, and 4 are all independent of each other — they only depend on step 1 completing successfully. Running them sequentially wastes time. This is a textbook fan-out scenario.
In the flow designer, after your "Validate inventory" SQL action, hover over the connector line between that action and whatever comes next. You'll see a "+" button. Click it, and instead of "Add an action," choose "Add a parallel branch." This creates a new branch lane alongside your existing actions.
Repeat this process to create a third branch. Your canvas should now show three vertical lanes all hanging off the bottom of the SQL validation step.
In the first branch, add your "Create a new record" Dataverse action for the CRM record. In the second branch, add "Send an email (V2)" from the Office 365 Outlook connector. In the third branch, add "Post message in a chat or channel" from the Teams connector.
After all three branches, Power Automate automatically converges them — you can add a final "Update order status" action that will only run once all three complete.
When you click "Code view" (or export the flow and inspect it), the parallel structure looks like this:
{
"actions": {
"Validate_Inventory": {
"type": "ApiConnection",
"inputs": { ... },
"runAfter": { "Get_Order_Details": ["Succeeded"] }
},
"Create_CRM_Record": {
"type": "ApiConnection",
"inputs": { ... },
"runAfter": { "Validate_Inventory": ["Succeeded"] }
},
"Send_Confirmation_Email": {
"type": "ApiConnection",
"inputs": { ... },
"runAfter": { "Validate_Inventory": ["Succeeded"] }
},
"Post_Teams_Notification": {
"type": "ApiConnection",
"inputs": { ... },
"runAfter": { "Validate_Inventory": ["Succeeded"] }
},
"Update_Order_Status": {
"type": "ApiConnection",
"inputs": { ... },
"runAfter": {
"Create_CRM_Record": ["Succeeded"],
"Send_Confirmation_Email": ["Succeeded"],
"Post_Teams_Notification": ["Succeeded"]
}
}
}
}
Notice how Create_CRM_Record, Send_Confirmation_Email, and Post_Teams_Notification all list Validate_Inventory in their runAfter — that's the fan-out. And Update_Order_Status lists all three in its runAfter with "Succeeded" — that's the fan-in. The engine will not execute Update_Order_Status until all three preceding actions have reached a terminal state.
Tip: You can manually edit
runAfterconditions in code view to create dependency graphs that are impossible to express in the visual designer. This is the primary reason experienced Power Automate developers get comfortable with the JSON layer.
Nothing stops you from creating parallel branches within parallel branches. Imagine your Dataverse CRM record creation actually requires three sub-steps: create the account, create the contact, create the opportunity. All three can be parallelized within that branch.
The designer starts to get visually messy at this point, but the underlying WDL handles it cleanly. Just be aware that each level of fan-out increases the total action count for your flow run, which counts against your daily action limits and your flow run duration limits.
Parallel branches are powerful for a fixed set of operations. But what about processing a collection — 50 orders, 200 employee records, or 1,000 rows from a database query? This is where Apply to each concurrency settings come in, and this is where most developers either dramatically undersell the feature or blow themselves up on connector throttle limits.
By default, Apply to each runs iterations sequentially: iteration 1 runs to completion, then iteration 2 starts, and so on. This is safe but often painfully slow. If each iteration takes 3 seconds and you have 100 items, you're looking at 5 minutes minimum for just the loop body.
In the designer, click the three-dot menu on your Apply to each action and select "Settings." You'll see a toggle for "Concurrency Control." Enable it, and a slider appears letting you set the degree of parallelism — from 1 (serial, the default) to 50.
When you set this to, say, 10, the engine dispatches up to 10 iterations simultaneously. As each iteration finishes, the next queued iteration starts. It's a sliding window, not a simple batch — you maintain up to 10 in-flight iterations at all times until the collection is exhausted.
Warning: Once you enable concurrency control on
Apply to each, you cannot use array variables as accumulators inside the loop. TheAppend to array variableandSet variableactions are not thread-safe in Power Automate's execution model. Multiple iterations writing to the same variable will race each other, and you will get corrupted or partial results. This is one of the most common bugs in concurrent Power Automate flows, and it fails silently — the flow succeeds, but your output variable has fewer items than expected.
This is where performance tuning becomes an engineering discipline rather than guesswork. Here's the framework:
Identify your bottleneck connector. Almost every loop body hits one external service more than others. For a SharePoint-heavy loop, SharePoint is your bottleneck. For SQL operations, it's SQL Server.
Find the connector's throttle limit. Each connector has documented request limits. SharePoint Online's REST API allows 600 requests per minute per connection. Office 365 Outlook's Send Email action has a limit of 30 calls per minute for the standard connector. Dataverse allows 6,000 API requests per 5 minutes per user.
Do the math. If your loop body makes 3 SharePoint calls per iteration and you have 100 items, that's 300 calls total. At 10 concurrent iterations with ~1 second per SharePoint call, you'd be attempting ~30 calls/second = 1,800 calls/minute — well above the 600 limit. You'd start seeing 429 throttle errors.
A concurrency of 3-4 for SharePoint-heavy loops is typically safe. For Dataverse with the higher limits, you can push to 10-15. For pure HTTP calls to a well-scaled API, you can often go higher.
The right formula:
Safe Concurrency = floor((Connector_Limit_Per_Minute / Calls_Per_Iteration) / 2)
The division by 2 is your safety margin — connector limits aren't perfectly smooth, and you'll often have background traffic from other flows.
Even with careful concurrency tuning, you'll hit 429 errors occasionally. Power Automate's connectors have built-in retry policies for throttle responses (they respect the Retry-After header), but the default retry configuration isn't always optimal.
In the settings for any action inside your loop, you can configure the retry policy. For loops with concurrency enabled, switch from "Default" to "Exponential Interval" with a reasonable max retry count (4-5) and interval starting at 20 seconds. This gives throttled requests time to clear before hammering the API again.
A race condition occurs when the correctness of your flow depends on the relative timing of concurrent operations — and that timing isn't guaranteed. Power Automate's parallel branches are particularly susceptible because developers instinctively think of branches as "separate things happening" without realizing they're contending for shared resources.
Imagine you're tracking the number of processed orders in a SharePoint list item — a simple integer field called ProcessedCount. Your concurrent loop has 20 iterations running simultaneously, each of which should increment this counter by 1 when it finishes.
Here's the naive (broken) implementation in pseudocode:
1. Get current ProcessedCount (reads: 5)
2. Calculate new value: 5 + 1 = 6
3. Update ProcessedCount to 6
With 20 concurrent iterations, many of them will execute step 1 before any of them complete step 3. They all read 5, they all calculate 6, and they all write 6. After 20 iterations, your counter reads 6 instead of 25. You've lost 19 increments.
This is a classic read-modify-write race condition, and it's every bit as real in Power Automate as it is in multithreaded application code.
A more insidious variant: two parallel branches both check "does this customer record exist in Dataverse?" and both find that it doesn't. Both proceed to create the record. You end up with duplicate customers. Downstream lookups then fail or return ambiguous results.
The check-then-act pattern is inherently racey when the check and act aren't atomic from the perspective of the shared resource.
As mentioned in the concurrency control section, flow variables are not safe to write from concurrent iterations. But this extends beyond Apply to each. If you have parallel branches that both write to the same flow-level variable, the last writer wins — and "last" is determined by indeterminate timing, not by any order you specify.
Branch A: Set variable 'orderSummary' to "Processed via Dataverse"
Branch B: Set variable 'orderSummary' to "Email sent to customer"
If these run in parallel, one of these will overwrite the other. Which one? You genuinely cannot predict it at design time.
Now for the constructive part: how you actually architect around these problems.
The most reliable way to prevent race conditions is to not have shared mutable state in the first place. If parallel branches never write to the same resource, they can never race each other.
Apply this by:
For the counter example: instead of incrementing a shared counter inside the loop, collect a boolean success flag from each iteration, then after the loop use a Filter array to count successful results and write that count once:
// After Apply to each completes
Compose action: length(filter(outputs('Apply_to_each_results'), item()?['success'] == true))
// Write this single computed value to SharePoint
For counter increments and similar operations, push the atomicity requirement to a system that can guarantee it. SQL Server is your friend here.
Instead of read-then-increment in your flow, use a SQL stored procedure that does the increment atomically:
UPDATE dbo.OrderMetrics
SET ProcessedCount = ProcessedCount + 1,
LastUpdated = GETUTCDATE()
WHERE BatchId = @BatchId;
You call this stored procedure from Power Automate ("Execute stored procedure" action in the SQL connector). SQL Server handles the row-level locking. Multiple concurrent calls to this procedure will serialize at the database level — no race condition possible.
Similarly, Dataverse supports calculated fields and rollup fields that aggregate counts at the platform level, removing the need for your flow to manage counters manually.
Some systems support optimistic concurrency — you read a record along with its version/ETag, write your update including the ETag, and the system rejects your write if the record was modified since you read it (returning a 409 Conflict).
SharePoint supports ETags on list items. When you use "Get item" and then "Update item," you can pass the @odata.etag value in the If-Match header on the update request. If another operation modified the item between your get and your update, the update will fail with a 412 Precondition Failed.
To implement this in Power Automate with the HTTP connector (since the SharePoint connector doesn't expose ETag control natively):
// GET the item first
GET https://yourtenant.sharepoint.com/sites/yoursite/_api/web/lists/getbytitle('Orders')/items(123)
Headers: { "Accept": "application/json;odata=verbose" }
// Extract ETag from response
// outputs('Get_Item')['body']['d']['__metadata']['etag']
// Then PATCH with If-Match
PATCH https://yourtenant.sharepoint.com/sites/yoursite/_api/web/lists/getbytitle('Orders')/items(123)
Headers: {
"If-Match": "<extracted_etag>",
"X-HTTP-Method": "MERGE",
"Content-Type": "application/json;odata=verbose"
}
If the 412 response comes back, you retry the entire read-then-write cycle. This is optimistic concurrency — you assume you won't have a conflict, proceed without locking, and handle the conflict if it does occur.
Tip: Optimistic concurrency is ideal when conflicts are rare. If your architecture regularly has multiple branches writing to the same record simultaneously, optimistic concurrency creates retry storms. In that case, redesign to eliminate the shared write — that's the fundamental problem.
For scenarios where you truly need to serialize access to a shared resource from concurrent flow iterations, you can implement a distributed mutex using Azure Blob Storage's lease mechanism.
Azure Blob Storage supports blob leases — an exclusive 15-60 second lock on a blob. Only the process holding the lease can modify the blob. All other lease acquisition attempts fail until the current holder releases or the lease expires.
The pattern in Power Automate:
1. HTTP POST to acquire lease on a "lock.json" blob
PUT https://yourstorage.blob.core.windows.net/locks/order-counter.lock?comp=lease
Headers: { "x-ms-lease-action": "acquire", "x-ms-lease-duration": "30" }
2. If 201 response (lease acquired):
a. Perform your critical section (read-modify-write)
b. HTTP DELETE or release the lease
3. If 409 response (lease already held):
a. Wait (Delay action, 2-5 seconds)
b. Retry acquisition
c. After N retries, fail gracefully
This is a genuinely robust distributed lock. The 30-second lease duration ensures the lock releases even if your flow iteration crashes mid-execution. The retry loop handles contention without deadlock.
The trade-off: every iteration that needs the lock pays a latency cost for the acquire/release cycle (~200-400ms per HTTP call), plus potential wait time if the lock is contended. Use this pattern surgically — only around the genuinely critical section, not around your entire loop body.
Often the cleanest solution to shared-state race conditions is repartitioning the work so concurrent operations never touch the same data.
For the order processing scenario: instead of all 50 concurrent iterations trying to update a single ProcessedCount field, partition your work so iteration N only touches records in bucket N. Use the item's index or a hash of the item's key to assign it to a partition. This is the "sharding" pattern applied to Power Automate flows.
// In your Apply to each, use the item's OrderId to determine which counter shard to update
// OrderId % 5 gives you partition 0-4
// Each partition gets its own counter field: Count_0, Count_1, ... Count_4
// After the loop, sum all five fields for the total
This eliminates contention entirely. It's more complex at write time, but dramatically simpler at runtime.
The visual designer limits you to straightforward fan-out/fan-in graphs. But real business logic often has more complex dependency requirements. Mastering runAfter in code view unlocks these patterns.
By default, the fan-in action only proceeds if all upstream branches succeed. But what if some branches are optional? What if a Teams notification failure shouldn't block the entire order from being marked as processed?
You can change the runAfter condition for the converging action to accept multiple terminal states:
"Update_Order_Status": {
"runAfter": {
"Create_CRM_Record": ["Succeeded"],
"Send_Confirmation_Email": ["Succeeded"],
"Post_Teams_Notification": ["Succeeded", "Failed", "Skipped", "TimedOut"]
}
}
Now Update_Order_Status runs as long as Create_CRM_Record and Send_Confirmation_Email succeed, regardless of what happened with the Teams notification. This is "best-effort" semantics for the non-critical branches.
You can then use result() expressions in your converging action to check what actually happened:
// Check if Teams notification succeeded
if(
equals(outputs('Post_Teams_Notification')?['statusCode'], 200),
'Teams notified',
'Teams notification failed - manual follow-up needed'
)
Consider: Branch A and Branch B can run in parallel. Branch C depends on both A and B. Branch D can run in parallel with C but depends on A. Branch E depends on both C and D.
{
"Branch_A": { "runAfter": { "Start": ["Succeeded"] } },
"Branch_B": { "runAfter": { "Start": ["Succeeded"] } },
"Branch_C": {
"runAfter": {
"Branch_A": ["Succeeded"],
"Branch_B": ["Succeeded"]
}
},
"Branch_D": { "runAfter": { "Branch_A": ["Succeeded"] } },
"Branch_E": {
"runAfter": {
"Branch_C": ["Succeeded"],
"Branch_D": ["Succeeded"]
}
}
}
This is a directed acyclic graph (DAG) of dependencies. The engine resolves it correctly — B and D run in parallel; C waits for both A and B; E waits for C and D. You get maximum parallelism while respecting all dependencies.
Warning: Power Automate does not detect circular dependencies at design time. If you hand-edit the JSON and accidentally create a cycle (Action A depends on B, B depends on A), the flow will fail at runtime with a cryptic error. Always verify your dependency graph is acyclic.
When a parallel branch contains more than 2-3 actions, wrapping it in a Scope action keeps the designer manageable and enables cleaner error handling. A scope acts as a container — you can configure runAfter on the scope as a unit, and you can check the scope's overall status in subsequent logic.
Structure your parallel branches as scopes:
Scope: "Provision_CRM_Record"
├── Create account
├── Create contact
└── Create opportunity
Scope: "Notify_Customer"
├── Send confirmation email
└── Schedule follow-up task
Scope: "Internal_Notifications"
├── Post to Teams
└── Update dashboard
In your convergence logic, check scope statuses:
result('Provision_CRM_Record')?[0]?['status']
// Returns 'Succeeded', 'Failed', etc.
This is significantly cleaner than checking every individual action when you have complex parallel branches.
Error handling in concurrent flows is substantially more complex than in serial flows because you can have partial failures — some branches succeed and some fail — and you need to decide what that means for your business logic.
If three branches run in parallel and one fails, what should happen?
By default, Power Automate fails the entire flow run if any action fails (and nothing in runAfter handles the failure state). But in many business scenarios, partial success is meaningful and shouldn't roll back everything.
Design your error handling strategy before building the flow, not after. The three common patterns are:
All-or-nothing: Any failure fails the entire flow and triggers compensating actions (rollbacks, cleanup, alerting). Use this when you need transactional consistency across all branches.
Best-effort: Failures in non-critical branches are logged and ignored. The flow succeeds if critical branches succeed. Use this when branches are truly independent and represent optional enhancements.
Graduated criticality: Some branches are critical (failure = fail the flow), some are important (failure = flag for review but continue), some are optional (failure = log and ignore). This is the most realistic model for complex business flows.
Wrap each parallel branch in a scope. After the convergence point, evaluate each scope's result:
// Expression to check if a scope failed
equals(result('Provision_CRM_Record')?[0]?['status'], 'Failed')
// Expression to get the error from a failed scope
result('Provision_CRM_Record')?[0]?['error']?['message']
Build a condition tree after convergence:
If Provision_CRM_Record failed:
→ Fail the flow (this is critical - no CRM record = broken data)
Else if Notify_Customer failed:
→ Create a SharePoint task item for manual follow-up
→ Continue flow
Else if Internal_Notifications failed:
→ Log to Application Insights
→ Continue flow
This pattern gives you fine-grained control without exploding the complexity of your runAfter conditions.
If your parallel branches each make changes that should be rolled back together when any one of them fails — you need the Saga pattern. Power Automate doesn't have native transaction support across connectors, so you implement it manually.
For each branch that can fail, define a compensating action:
In your error handling scope, run the appropriate compensating actions based on which branches succeeded before the failure:
// Check which branches succeeded before triggering compensation
If Create_CRM_Record succeeded AND Create_SQL_Record failed:
→ Delete CRM record (compensate)
→ Log compensation
→ Fail the flow
This is genuinely complex to implement in Power Automate's visual environment, and it's a legitimate reason to consider whether a flow with full distributed transaction requirements should live in Power Automate or in a code-based orchestration system (Durable Functions, for example). Know your tool's boundaries.
Let me give you concrete numbers to inform your architectural decisions.
For a four-action parallel branch scenario (Dataverse write, email send, Teams post, SQL log) measured in Power Automate Premium:
| Configuration | Wall Clock Time | Notes |
|---|---|---|
| Serial | ~12-15 seconds | Each action ~3-4s |
| 3 parallel branches | ~4-6 seconds | Near-ideal parallelism |
| 4 parallel branches | ~3-5 seconds | Marginal gain; connector startup overhead dominates |
You get diminishing returns beyond 3-4 parallel branches for most connector operations because the bottleneck shifts from "waiting for other branches to finish" to "connector initialization and authentication overhead" — which is roughly 1-2 seconds per connector call regardless of what the connector actually does.
Processing 100 SharePoint list item updates with a single Get item + Update item per iteration:
| Concurrency Setting | Total Time | Notes |
|---|---|---|
| 1 (serial) | ~8-10 minutes | 5s per iteration |
| 5 | ~2-3 minutes | Near-linear scaling |
| 10 | ~90-120 seconds | Throttle pressure begins |
| 20 | ~75-120 seconds | Frequent throttle retries, inconsistent |
| 50 | ~90-150 seconds | Often slower than 10 due to throttle storms |
The sweet spot for SharePoint is typically 5-8 concurrent iterations. Beyond that, throttle retries eat your gains.
For SQL Server (direct connector, not HTTP), you can often push to 20-30 concurrent iterations before connection pool limits become the bottleneck.
Parallel branches are not free from a platform quota perspective. Each branch and each iteration counts against:
Monitor your flow run analytics regularly when using high concurrency. Look for action execution time outliers (which indicate throttling) and payload size trends.
Build the following flow from scratch. This exercise is designed to force you to confront every concept in this lesson.
You have a SharePoint list called "Weekly Reports" with 30 items. Each item has fields: Title, AssignedTo (email), Status (choice: Pending/Processing/Complete/Failed), DataverseRecordId (text), and ProcessingNotes (text).
Your flow should trigger on a manual button press (Instant cloud flow) and:
Status equals "Pending"Status to "Processing"AssignedTo address notifying them their report is being processedStatus to "Complete" and stores the Dataverse record ID in DataverseRecordIdProcessingNotes, but other items should continue processingChallenge 1: You cannot use a flow variable to accumulate success/failure counts inside a concurrent loop. How will you produce the summary counts at the end? (Hint: use result() on the scope wrapping your loop body, then filter by status.)
Challenge 2: The Dataverse record ID needs to be stored back in SharePoint, but it's only available after the Dataverse create action. Ensure your Update item action for the "Complete" state correctly references the Dataverse action's output within the same iteration scope.
Challenge 3: Test what happens when you deliberately cause the Dataverse action to fail (use a bad table name) and verify that the SharePoint item status correctly shows "Failed" rather than remaining in "Processing."
Challenge 4: After fixing Challenge 3, re-enable the correct Dataverse configuration and run the flow against 30 items. Check your run history for any 429 errors. If you see them, tune your concurrency setting down and re-run.
Symptom: Your array variable has fewer items than expected after a concurrent Apply to each loop. The flow reports success.
Cause: Append to array variable and Set variable are not atomic. Concurrent writes drop updates.
Fix: Don't accumulate inside concurrent loops. Use result() expressions after the loop to analyze outcomes.
Symptom: A race condition you thought you'd avoided still occurs occasionally, but not every run.
Cause: "Parallel" means dispatched at the same time, not starting at the same time. Under load, branch start times can vary by seconds.
Fix: Never rely on relative timing between branches. Use proper synchronization (database atomicity, blob leases) for any shared state access.
Symptom: Flow runs start failing or running much slower than expected after you increased concurrency. You see 429 errors in action details.
Cause: You've exceeded connector rate limits, and retries are creating a thundering herd problem.
Fix: Apply the safe concurrency formula from earlier in this lesson. Check connector documentation for rate limits. Use exponential backoff retry policies.
Symptom: If any one of your parallel branches fails (even a non-critical Teams notification), the entire flow fails.
Cause: The default runAfter on the convergence action only accepts "Succeeded" for all upstream branches.
Fix: Edit runAfter conditions on your convergence action to include "Failed", "Skipped", and "TimedOut" for non-critical branches.
Symptom: After a parallel branch failure, your error handling can't determine which branch failed or what the error was.
Cause: Without scope actions wrapping each branch, there's no clean way to check branch-level status.
Fix: Always wrap multi-action parallel branches in Scope actions. Use result('Scope_Name')?[0]?['status'] in convergence logic.
Symptom: Flow fails immediately at runtime with an error like "workflow definition is invalid."
Cause: You created a dependency cycle when editing code view.
Fix: Draw your dependency graph on paper before editing JSON. Verify the graph is a DAG (directed acyclic graph) — you should be able to find a topological ordering of all actions.
Symptom: Flows with high-concurrency loops start failing with storage-related errors at high item counts.
Cause: Each action's input/output is stored in run history. Large responses (big SQL result sets, large Dataverse records) at high concurrency can exceed per-run storage limits.
Fix: Use Select expressions to pare down action outputs to only the fields you need. This keeps the run history payload small and also speeds up expression evaluation in subsequent actions.
You've covered a lot of terrain in this lesson. Let's consolidate what you've built in your understanding:
Parallel branches in Power Automate are expressed as multiple actions sharing the same runAfter dependency. The engine dispatches them concurrently to a worker pool, and a convergence action waits for all upstream branches to reach a terminal state. This gives you genuine fan-out/fan-in parallelism without any orchestration code.
Apply to each concurrency control transforms serial loop processing into a sliding-window parallel execution model. The right concurrency level is constrained by your bottleneck connector's rate limits, not by what "feels right." Always do the math.
Race conditions are real and silent. The most common forms in Power Automate are concurrent writes to flow variables, read-modify-write patterns on shared records, and check-then-act patterns where the check and act aren't atomic. The primary defenses are eliminating shared mutable state, pushing atomicity requirements to the database layer, and using distributed locks when you truly need them.
Error handling in concurrent flows requires explicit design. Partial failures are the norm at scale. Use scope actions, runAfter multi-state conditions, and the result() expression to build graduated criticality handling that reflects your actual business requirements.
Performance tuning is empirical. The benchmarks in this lesson give you starting points, but your actual performance depends on your tenant's load, the time of day, your specific connector configurations, and the shape of your data. Monitor, measure, and iterate.
Azure Durable Functions with Power Automate HTTP triggers: When your concurrency requirements exceed what Power Automate can express cleanly, Durable Functions' fan-out/fan-in patterns give you full code control with orchestration infrastructure provided.
Dataverse plug-ins for server-side logic: Moving complex atomic operations into Dataverse plug-ins eliminates entire categories of race conditions by executing logic server-side under transaction control.
Application Insights integration: Learn to emit custom telemetry from Power Automate flows using the HTTP connector, so you have a proper observability story for high-concurrency flows in production.
Power Automate Process Advisor: Profile your flows to find the actual bottleneck actions before optimizing. The intuitive bottleneck and the real bottleneck are often different.
Premium connectors and connection pooling: Understand how premium connector credentials are shared across concurrent executions and how to avoid authentication bottlenecks at high concurrency.
Parallelism in Power Automate is one of those capabilities where the distance between "I know this exists" and "I can use this reliably in production" is genuinely large. You've now closed most of that gap.
Learning Path: Flow Automation Basics