When hundreds of flow runs launch simultaneously, connectors overload and pipelines collapse. This lesson teaches you how to calculate and configure concurrency limits, size your queue depth, and build a layered throttling architecture that protects downstream systems without sacrificing throughput.

Picture this: your ERP system fires a batch of 500 purchase order events at 9:00 AM when the overnight job completes. Your Power Automate flow picks up each event from a SharePoint list, calls an external logistics API to validate shipping addresses, writes the result to Dataverse, and sends a confirmation email. For a single run, this takes maybe eight seconds. But when 500 runs launch simultaneously, that logistics API — which supports 20 concurrent connections — starts returning 429s. Dataverse connector calls pile up against platform throttle limits. The email connector queues become a traffic jam. By 9:05 AM, half your runs have failed, and your on-call engineer is staring at a wall of red in the flow run history.
This is a connector overload problem, and it's one of the most common failure modes in enterprise Power Automate deployments. The good news: Power Automate gives you several concrete levers to control exactly how many runs execute simultaneously, how many runs wait in queue, and how your flows behave when those limits are hit. Understanding and correctly configuring these controls is the difference between a pipeline that degrades gracefully under load and one that falls apart spectacularly.
By the end of this lesson, you'll be able to configure flow-level concurrency settings with full understanding of what they protect, size your queue depth appropriately for your throughput requirements, choose the right rejection and retry strategy, combine these controls with downstream connector rate limits, and troubleshoot the most common overload patterns in production pipelines.
What you'll learn:
You should already be comfortable with the basics of trigger configuration, solution-aware flows, and connector behavior in Power Automate. This lesson assumes you've worked with high-volume flows before and understand that Microsoft's platform-level API request limits exist — we'll be working above that layer, at the architectural level. Familiarity with parallel branching and concurrency control fundamentals will help you contextualize what we're covering here.
Before you can fix a connector overload problem, you need to understand exactly where the pressure is coming from. Power Automate runs are not lightweight threads — each run is a stateful execution context managed by the platform, and every action inside that run consumes API request quota from the triggering user or service principal's entitlement.
When your trigger fires 500 times in rapid succession (say, from a Service Bus queue with 500 messages), the default behavior without any concurrency control is that all 500 runs start as quickly as the platform can schedule them. The platform does impose some internal rate controls, but they are not tuned to your specific connector's limits. If your logistics API accepts 20 concurrent requests, and 300 flow runs are all simultaneously trying to call it, you will get 429 responses — throttle errors — and those runs will either fail or spend time retrying, creating a secondary wave of load.
This problem compounds when you consider that connectors operate within shared connection infrastructure. The SharePoint connector, the Dataverse connector, and your HTTP-based connectors all have independent rate limit profiles:
The key insight is that connector rate limits are about simultaneous resource consumption, not just request volume. Running 20 calls sequentially over 10 seconds is very different from running 20 calls simultaneously, even if the total request count is identical.
Key insight
Concurrency limits in Power Automate operate at the flow run level, not the action level. Limiting concurrent runs to 10 doesn't mean you have 10 simultaneous API calls — it means you have 10 simultaneous flow executions, each of which may contain multiple sequential actions. This is an important distinction when calculating the actual throughput pressure on a connector.
Every trigger-based flow has a concurrency setting that controls how many instances of the flow can execute simultaneously. When you enable concurrency control on a trigger, Power Automate maintains a queue of pending runs. Runs that cannot start immediately (because the concurrency limit is reached) sit in the queue until a running instance completes.
Here's the mental model: think of it as a pool of workers (concurrent runs) and a waiting room (the queue). The concurrency limit is the number of workers. The queue depth is the size of the waiting room. When a new trigger fires and all workers are busy:
This is a load-shedding mechanism, not a back-pressure mechanism. Understanding that distinction is crucial: the platform does not automatically retry skipped runs unless your trigger source implements its own retry logic (which Service Bus queues do, which is one major reason to use them for high-volume work).
To enable and configure concurrency on a trigger, open your flow in the designer and click the trigger action. In the settings panel for the trigger (the gear icon or "Settings" tab depending on your designer version), you'll find a "Concurrency Control" toggle. Once you flip it on, two parameters appear:
Warning
The moment you enable concurrency control and set a Degree of Parallelism below 100, you are potentially changing the behavior of flows that previously ran without ordering constraints. If your flow modifies shared state (a SharePoint list item, a Dataverse record, a counter in Azure Table Storage), enabling a concurrency limit of 1 essentially serializes your pipeline. This eliminates race conditions but also eliminates parallelism. Always evaluate the trade-off explicitly.
In the raw flow definition (which you can inspect by opening the flow in a solution and exporting, then examining the JSON), the concurrency setting looks like this:
"triggers": {
"When_a_new_item_is_created": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['sharepointonline']['connectionId']"
}
},
"method": "get",
"path": "/datasets/@{encodeURIComponent(encodeURIComponent('https://contoso.sharepoint.com/sites/SupplyChain'))}/tables/@{encodeURIComponent(encodeURIComponent('PurchaseOrders'))}/onnewitems"
},
"recurrence": {
"frequency": "Minute",
"interval": 1
},
"splitOn": "@triggerBody()?['value']",
"operationOptions": "SynchronousPattern",
"runtimeConfiguration": {
"concurrency": {
"runs": 15,
"maximumWaitingRuns": 50
}
}
}
}
The runs value is your Degree of Parallelism. The maximumWaitingRuns is your queue depth. Notice that these are nested inside runtimeConfiguration — this is an important detail if you're managing flows as solution-aware deployments, because these values need to be correctly set in your flow definition before deployment or configured post-deployment via environment-specific overrides.
This is where most teams get it wrong. They either pick an arbitrary number like 10 or 25, or they leave the default (unlimited) because they're afraid of slowing things down. The correct approach is to calculate a target concurrency based on your connector's limits and your flow's action latency.
The formula is straightforward:
Target Concurrent Runs = min(
floor(Connector Rate Limit / Actions Per Run That Hit That Connector),
Connector Max Concurrent Connections
)
Let's work through a real example. Your purchase order flow calls the logistics API once per run, and the logistics API supports 20 concurrent connections (documented in their API spec) and 200 requests per minute sustained throughput. Each call takes roughly 2 seconds.
Step 1 — Concurrent connection limit check: Maximum concurrent connections = 20 Actions per run hitting that connector = 1 → Upper bound from concurrency = 20
Step 2 — Throughput check: 200 requests per minute = 3.33 requests per second sustained Each run takes 2 seconds for that action At 20 concurrent runs, you'd be issuing requests at a rate governed by completion time: 20 runs / 2 seconds average = 10 completions per second, which means 10 new requests per second = 600 requests per minute → That's 3x the sustained limit
Step 3 — Calculate based on sustained throughput: 3.33 requests/second × 2 seconds per request = 6.66 concurrent runs max → Round down to 6 for safety margin
So the right answer for this API is a concurrency limit of 6, not 20 — even though the API supports 20 concurrent connections. The sustained throughput limit is the binding constraint.
Tip
Always load test your target connector in a non-production environment before choosing your concurrency limit. Published API rate limits are often the burst limit, not the sustained limit. A logistics API that says "20 concurrent connections" may start returning 429s after 30 seconds of sustained load at that level. Use your test environment to find the real number. See testing flows before release for environment setup guidance.
Now add a safety margin. In production systems, you always want to leave headroom for:
A common practice is to target 70-80% of the calculated limit. In our example: 6 × 0.75 = 4.5, round to 5 concurrent runs.
The maximumWaitingRuns setting is your buffer. Think carefully about what this number means for your specific scenario.
If you set maximumWaitingRuns to 0, there is no queue — any run that can't start immediately is dropped. This is appropriate when runs represent real-time events where stale data is worse than no data (e.g., a live dashboard update where a 5-minute-old update is useless).
If you set maximumWaitingRuns to 100, runs can back up for a long time before being processed. Whether this is acceptable depends on your SLA for processing latency and whether the data in the run context is still valid after a long wait.
The sizing formula is:
Queue Depth = Expected Burst Size - Concurrent Runs
Example:
Burst of 500 items at 9 AM
5 concurrent runs
Each run takes 8 seconds
Processing rate: 5 runs / 8 seconds = 0.625 runs per second
Time to clear 500 items: 500 / 0.625 = 800 seconds (~13 minutes)
Queue must hold: 500 - 5 = 495 items
But if your SLA is 30 minutes for all items:
Items processed in 30 minutes = 0.625 × 1800 = 1,125 items
→ Your concurrency/latency can handle 500 items in 13 minutes, so a queue of 495 is sufficient
In this case, set maximumWaitingRuns to at least 495, probably 550 with buffer. The practical maximum is 100 for most trigger types in Power Automate (as of current platform capabilities) — if your burst size exceeds that, you need a different architecture (more on this below).
Warning
The maximum maximumWaitingRuns value Power Automate enforces varies by trigger type and license tier. For many standard trigger types, the platform caps this at 100. If your burst volume regularly exceeds concurrent runs + 100, you will lose trigger events unless you build an external queuing layer. Azure Service Bus is the right solution for this scenario — see event-driven automation with Azure Service Bus for how to build that architecture.
This is the part that bites teams in production: when a trigger fires and both the concurrency pool and the queue are full, the run is not queued — it is skipped. Power Automate records this in the run history as a skipped or cancelled run, depending on the trigger type.
For different trigger types, the rejection behavior differs:
SharePoint/Dataverse polling triggers: The item remains in the source. On the next poll cycle, the trigger may pick it up again. However, this creates a problematic pattern: your poll interval fires again before the queue drains, picks up the same items plus new ones, and the problem compounds. This is why polling triggers are fundamentally poorly suited to high-volume burst scenarios.
HTTP request triggers: The caller receives a 429 response if concurrency control is active and the queue is full. This at least gives the caller a signal to retry. If you're using Azure API Management in front of your HTTP endpoints, APIM can absorb the 429 and retry or queue the request itself.
Service Bus triggers: The message is not acknowledged, so Service Bus retains it and redelivers it after the lock timeout. This is the most resilient behavior — Service Bus acts as your external queue, effectively extending your queue depth to the Service Bus queue's capacity. This is the architecture you should use for true high-volume pipelines.
Scheduled triggers: If a scheduled run is skipped, it's simply missed. If your business logic requires every scheduled execution to complete, you need to build compensating logic that detects missed runs.
A single flow-level concurrency limit is often insufficient for complex enterprise pipelines. Real-world architectures involve parent flows, child flows, and multiple downstream connectors, each with its own rate limit profile. You need to think about concurrency control at multiple layers.
In this pattern, the parent flow handles triggering and concurrency, while child flows handle the actual work. You set the concurrency limit on the parent trigger, which effectively gates how many child flow invocations happen simultaneously.
This is effective when all your child flows call the same downstream connector, because the parent's concurrency limit directly controls the load on that connector.
Parent Flow (concurrency: 5, queue: 100)
└─ Trigger: Service Bus message received
└─ Initialize correlation ID
└─ Call child flow: "Process Purchase Order"
└─ Validate address via Logistics API (2 sec)
└─ Write result to Dataverse
└─ Send confirmation email
The parent flow call to the child flow is synchronous by default (using the "Run a Child Flow" action), so the parent's run slot stays occupied until the child completes. This means your concurrency limit of 5 on the parent correctly caps you at 5 simultaneous logistics API calls. See orchestrating child flows for scalable architecture for detailed patterns on structuring this correctly.
Even with concurrency control on the trigger, you should configure retry policies on individual connector actions that are likely to receive 429 responses. This provides a second layer of defense when transient load spikes push you briefly over your concurrency calculation.
In the action settings (the "..." menu on any action, then "Settings"), configure:
The JSON for this in the flow definition:
"Validate_Shipping_Address": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://logistics-api.contoso.com/v2/validate",
"headers": {
"Authorization": "Bearer @{body('Get_API_Token')?['access_token']}",
"Content-Type": "application/json"
},
"body": {
"orderId": "@triggerBody()?['OrderId']",
"addressLine1": "@triggerBody()?['ShipToAddress1']",
"city": "@triggerBody()?['ShipToCity']",
"postalCode": "@triggerBody()?['ShipToPostalCode']"
}
},
"retryPolicy": {
"type": "exponential",
"count": 4,
"interval": "PT5S",
"minimumInterval": "PT5S",
"maximumInterval": "PT60S"
},
"runAfter": {
"Get_API_Token": ["Succeeded"]
}
}
Tip
Exponential backoff on retries is essential — fixed-interval retries create synchronized retry storms. If 5 concurrent runs all hit a 429 and retry after exactly 5 seconds, you create a second synchronized burst of 5 requests that may itself get throttled, leading to cascading retries. Exponential backoff with jitter (which Power Automate applies automatically when you use the "Exponential" policy) spreads retries across a time window. Learn more about this pattern in implementing exponential backoff and circuit breaker patterns.
If your flow processes items from a list or query rather than item-by-item from a trigger, you need to control not just how many flows run simultaneously but how many items each run processes. This is batch size control, and it complements concurrency control.
A flow with concurrency of 5 that processes 100 items per run creates 500 simultaneous API calls — very different from a flow with concurrency of 5 that processes 10 items per run in sequence (50 API calls total, sequential within each run). Combining batching and chunking strategies with concurrency control gives you precise control over throughput.
The splitOn property on a trigger is one of the most powerful and most misunderstood features in Power Automate. When a trigger returns an array of items (as SharePoint's "When a new item is created" can when polling catches up), splitOn tells Power Automate to create one run per item rather than one run per trigger poll.
Here's the critical implication: splitOn and concurrency control interact. When you have splitOn enabled and the trigger returns 200 items, Power Automate creates 200 run instances. Your concurrency setting of 15 means 15 run immediately and 185 wait in queue (up to your maximumWaitingRuns limit). Any beyond that are dropped.
This is powerful because it means you can process large batches of items with flow-level isolation per item, controlled by your concurrency setting. But it requires your queue depth to accommodate the full expected burst minus your concurrent run count.
"triggers": {
"When_items_are_in_SharePoint": {
"splitOn": "@triggerBody()?['value']",
"runtimeConfiguration": {
"concurrency": {
"runs": 5,
"maximumWaitingRuns": 95
}
}
}
}
In this configuration, a burst of 100 SharePoint items will result in 5 running immediately and 95 queued. Item 101 and beyond would be dropped. This is why we sized the queue at 95 — to accommodate exactly the burst of 100 with the right headroom.
Note
splitOn has a practical limit of 100 items per trigger poll. This is a platform-enforced limit, not a configuration option. If your source system delivers more than 100 items in a single poll cycle, you'll need to handle pagination on the trigger or switch to an event-driven architecture where items are published individually. Review handling pagination and throttling in Power Automate for techniques to manage this.
Concurrency limits only protect you if you know when they're being hit. A flow that's regularly dropping runs due to queue overflow is failing silently — you won't see failed runs, you'll see skipped runs, which are easy to miss in operational monitoring.
The signals to watch for:
1. Skipped runs in flow run history: In the Power Automate portal, filter the run history by "Skipped" status. If you see skipped runs correlating with your known high-load periods, your queue is overflowing.
2. Run start latency: Compare the trigger time (when the trigger fired) against the run start time (when the first action executed). Long gaps indicate runs sitting in queue. You can capture this via run context and trigger metadata.
3. Downstream 429 rate: If your connector action is receiving 429 responses despite your concurrency limit, either your limit is too high or another consumer is hitting the same API.
A production-ready monitoring setup should log this data to Azure Application Insights. At the start of each run, emit a custom event with the trigger timestamp and current timestamp:
triggerTime: @{triggerOutputs()?['headers']?['x-ms-trigger-timestamp']}
runStartTime: @{utcNow()}
queueLatencySeconds: @{div(
sub(
ticks(utcNow()),
ticks(triggerOutputs()?['headers']?['x-ms-trigger-timestamp'])
),
10000000
)}
When queue latency starts climbing consistently, it's a leading indicator that your concurrency limit or queue depth needs adjustment. For comprehensive telemetry setup, see building a Power Automate monitoring and alerting system.
Let's put this all together in a realistic scenario. You'll configure a flow that processes purchase orders from a SharePoint list with proper concurrency controls and retry logic for an external validation API.
Address validation API: 30 req/min = 0.5 req/sec sustained, 8 concurrent max Each API call takes approximately 3 seconds (measured in your test environment) Sustained throughput check: 0.5 req/sec × 3 sec = 1.5 concurrent runs max Concurrent connection check: 8 connections Binding constraint: Sustained throughput → 1.5 → round to 2 concurrent runs With 70% safety margin: 2 × 0.7 = 1.4 → 1 concurrent run (fully serialized)
This is a slow API. With 1 concurrent run taking ~5 seconds per item (including Dataverse write and SharePoint update), you can process 12 items per minute. 200 items takes ~17 minutes. If your SLA allows that, proceed with concurrency = 1. If not, you need to either negotiate higher API limits, add a caching layer, or batch the validation calls.
For this exercise, let's assume the SLA is 30 minutes and proceed.
In the flow designer:
For your burst of 200 items, items 102-200 will be dropped by this configuration. That's a problem. Since SharePoint polling picks up new items on the next poll, the dropped items will be retried on the next poll cycle — but this creates a fragile retry pattern. For a real production scenario with 200-item bursts, you should use a Service Bus queue as the trigger source.
On the HTTP action that calls the address validation API, set:
On the Dataverse "Create a new row" action:
Dataverse uses a fixed policy here because Dataverse throttling is usually brief (a few seconds) and predictable, while the external API benefits from the exponential spread.
At the very start of the flow (after the trigger, before any other action), add a "Compose" action to calculate and store the queue latency:
Name: Calculate_Queue_Latency
Inputs: @{div(
sub(
ticks(utcNow()),
ticks(triggerOutputs()?['headers']?['x-ms-trigger-timestamp'])
),
10000000
)}
Then add a "Send an HTTP request" action to log to Application Insights:
{
"name": "PurchaseOrderProcessingStarted",
"properties": {
"orderId": "@{triggerBody()?['OrderId']}",
"queueLatencySeconds": "@{outputs('Calculate_Queue_Latency')}",
"flowRunId": "@{workflow()['run']['name']}",
"environment": "@{workflow()['tags']['environmentName']}"
}
}
Since you know items 102-200 will be dropped when the queue is full, build a compensating scheduled flow:
Status = "Pending" AND Created > [24 hours ago] AND Modified < [20 minutes ago]This compensating flow catches any items that slipped through the cracks, giving you an eventual-consistency guarantee even when the primary flow drops items during peak load.
Fully serializing every flow is the easiest way to create a different kind of problem: throughput so low that your pipeline can never catch up with incoming load. A queue that grows faster than it drains is a disaster in slow motion. Calculate the right concurrency based on your connector's actual limits, not fear of overload.
If you call a child flow from a parent with a concurrency limit of 5, but you've also independently triggered the same child flow from another parent or a scheduled trigger, the child flow may receive more concurrent invocations than you accounted for. Each child flow execution consumes resources independently. If you're using child flows for shared processing logic, consider also setting concurrency limits on the child flow itself as a safety net.
A retry is a second API call. If your concurrency limit generates 5 initial calls and each call has a 30% chance of needing one retry, you're actually generating up to 6.5 API calls per processing cycle, not 5. At high volume, retries can push you over your connector limit even when your initial concurrency calculation was correct. Build in more headroom when your target API has a known reliability issue or when you're integrating with a system that throttles aggressively.
Skipped runs don't appear as failures in most monitoring setups by default. If you're alerting on flow failure counts, you'll completely miss queue overflow. Always add an explicit check for skipped/cancelled runs in your monitoring and alerting system.
The most dangerous mistake is configuring these settings in production without validating them first. Use a test environment populated with realistic data volumes, fire the actual expected burst load, and observe what happens. Does queue latency stay acceptable? Do you see 429s in the run history? Does the downstream system show elevated error rates? See testing flows before release for a systematic approach to load testing in non-production environments.
Symptom: Your concurrency limit is set to 5, but the logistics API is still returning 429s.
Diagnosis: Check whether another flow or another system is also calling the same API using the same credentials or the same API quota pool. Power Automate's concurrency control only governs its own trigger invocations — it cannot control what other callers do to the API.
Solution: Implement a centralized API gateway pattern. Route all calls to the logistics API through Azure API Management, configure rate limiting at the APIM level, and have Power Automate (and all other callers) go through APIM. APIM becomes the single choke point, enforcing the API's limits regardless of which caller is generating the load.
Symptom: Within seconds of your trigger firing, the queue is full and runs are being dropped.
Diagnosis: Your concurrency × average run duration throughput is too low for your incoming event rate. You're not catching up between batches.
Solution: Either increase concurrency (if your connector's rate limit allows it), reduce run duration (optimize your actions, add caching, remove unnecessary steps), or move to an asynchronous architecture where the trigger source (Service Bus, Azure Storage Queue) provides the buffering and Power Automate pulls at its own pace.
Concurrency control and queue depth management are not optional niceties in enterprise Power Automate pipelines — they are production requirements whenever your flow touches any external system with rate limits, which is virtually always. The core discipline is treating your flow's concurrency limit as a derived quantity: start with your connector's rate limits and latency profile, apply the throughput formula, add a safety margin, and validate under realistic load.
The controls you've learned here — the runs and maximumWaitingRuns settings in runtimeConfiguration, retry policies with exponential backoff, splitOn behavior and its queue implications, layered concurrency across parent and child flows — form a coherent throttling architecture that protects your downstream systems without over-serializing your pipeline.
The key principles to carry forward:
From here, the natural next areas to explore are building the Service Bus-backed architecture that removes the queue depth ceiling entirely, and implementing correlation IDs so you can trace individual items through a multi-flow pipeline end-to-end. For the former, start with event-driven automation with Azure Service Bus. For the latter, implementing correlation IDs and distributed tracing will give you the observability layer your production pipeline needs to be truly operable.