Architecture

Fix Dataverse 429 Errors: Service Protection Limits for Integrations

Understand Dataverse service protection API limits, read the 429 error codes and headers, honour Retry-After, and design bulk loads and Azure Data Factory copies that stay within them.

14 min read
On this page

A Dataverse integration gets 429 Too Many Requests when one user account, including an application user, exceeds a service protection limit on a web server: 6,000 requests or 20 minutes of combined execution time in a five-minute sliding window, or more than 52 concurrent requests. The fix is to pause for the number of seconds in the Retry-After response header, then design the load so it stays near the limit instead of exceeding it: cap parallelism at the x-ms-dop-hint value, disable the server affinity cookie, use bulk messages sensibly and keep plug-ins fast. Retrying blindly or adding more threads makes the Retry-After periods longer.

Who this is for and what you will have at the end

This guide is for developers and integration architects who load or synchronise Dataverse data from custom code, PowerShell, middleware or Azure Data Factory and see 429 responses, throttling faults, or loads that stall for minutes.

At the end you will have:

  • A clear picture of the three service protection limits and how they differ from Power Platform request limits.
  • A way to identify which limit a failing request hit.
  • Retry logic that respects Retry-After in PowerShell and .NET.
  • A throughput design: degree of parallelism, affinity, batching and bulk operation messages.
  • Azure Data Factory copy activity settings that work with the limits rather than against them.

How Dataverse service protection limits work

Dataverse evaluates service protection limits per user. Every authenticated identity has an independent limit, and Microsoft states that application users get the same limits as everyone else. The limits are also enforced per web server: most environments have more than one, trial environments have only one, and the number depends on factors Dataverse manages, including how many user licences you purchase.

Three facets are measured:

MeasureDefault limit per web serverWindowError codeHex code
Number of requests6,000Five-minute sliding window-21470159020x80072322
Combined execution time20 minutes (1,200 seconds)Five-minute sliding window-21470159030x80072321
Concurrent requests52 or higherImmediate-21470158980x80072326

The three facets close each other's loopholes. Bundling operations in batches reduces the request count, so the execution time limit applies. Sending many requests at once before the window catches up is stopped by the concurrency limit. Microsoft notes that these are default values, they can change, and they might vary between environments.

Two behaviours matter for design:

  • You can exceed the request and execution time limits for a five-minute period before enforcement starts, but exceeding the concurrent request limit returns an error immediately.
  • If a client keeps sending demanding requests after being throttled, Dataverse extends the Retry-After duration. Aggressive retrying lengthens the pauses and lowers total throughput.

What doesn't count

Data operations performed by plug-ins and custom workflow activities run in the sandbox service and don't use the public API endpoints, so their requests don't count towards the request limit. Their computation time does count: it's added to the execution time of the request that triggered them. Dataverse search (api/search) has separate rules, with a limit of one request per second per user.

Service protection limits versus Power Platform request limits

These are two different systems, and teams often confuse them.

AreaService protection limitsPower Platform request limits
PurposeProtect availability from sudden surgesEntitlement based on licences
WindowFive minutes, plus concurrency24 hours
ScopePer user, per web serverPer licensed user; non-licensed identities share a tenant pool
Symptom429 or throttling fault with Retry-AfterPossible throttling for sustained overage
Effect of batchingBatch counts as one request, but execution time accruesEvery create, update or delete inside a batch or bulk request counts

Application users, non-interactive users, administrative users and the SYSTEM user all draw from a shared tenant-level pool of non-licensed requests. Microsoft's FAQ is explicit that these identities don't get their own tenant-level limit each. Batching is not a way around entitlement limits.

Prerequisites

  • An identity that calls the Web API or the SDK for .NET, normally an application user. If you haven't set one up, see Dataverse application users for server-to-server auth.
  • The environment URL, for example https://contoso.crm.dynamics.com.
  • Access to the integration's logs, including HTTP status codes and response headers.
  • For PowerShell examples: PowerShell 7.4 or later and the Az PowerShell module 11.1.0 or later, as listed in Microsoft's Web API PowerShell guidance.

Step 1: Identify which limit you hit

Read the error code from the response body, not just the status code. With the Web API the hex code is in the error payload; with the SDK for .NET the numeric code is on the OrganizationServiceFault.

The messages are specific:

0x80072322  Number of requests exceeded the limit of 6000 over time window of 300 seconds.
0x80072321  Combined execution time of incoming requests exceeded limit of 1,200,000 milliseconds over time window of 300 seconds. Decrease number of concurrent requests or reduce the duration of requests and try again later.
0x80072326  Number of concurrent requests exceeded the limit of 52.

What each one usually means:

  • Number of requests: many small sequential or parallel calls from one identity. Consider batching or bulk messages.
  • Execution time: large batches, complex queries, solution imports, or slow synchronous plug-ins on the target table. Reduce batch size or plug-in cost.
  • Concurrent requests: too many threads. Cap parallelism.

While debugging, the Web API returns two headers that show the remaining budget:

HeaderMeaning
x-ms-ratelimit-burst-remaining-xrm-requestsRemaining number of requests for this connection
x-ms-ratelimit-time-remaining-xrm-requestsRemaining combined duration for all connections using the same user account

Microsoft says not to use these values to control how many requests you send; they are for debugging, and they reset if you connect to a different server.

Step 2: Honour Retry-After

Every service protection error carries a wait time. The Web API returns a Retry-After header in seconds; the SDK for .NET puts a TimeSpan in OrganizationServiceFault.ErrorDetails under the key Retry-After. Wait that long, then resend the same request.

SDK for .NET

Use ServiceClient (Microsoft.PowerPlatform.Dataverse.Client) or CrmServiceClient. These classes handle service protection errors for you; CrmServiceClient versions after 9.0.2.16 automatically pause and resend after the Retry-After period. If your code still uses OrganizationServiceProxy or OrganizationWebProxyClient, replace them; OrganizationServiceProxy is deprecated.

PowerShell

Invoke-RestMethod retries status codes from 400 to 599 when you set -MaximumRetryCount, and for a 429 with a Retry-After header it uses that value instead of -RetryIntervalSec. Retrying every 4xx slows scripts down, so Microsoft's sample wraps the call and only retries 429:

function Invoke-ResilientRestMethod {
   param (
      [Parameter(Mandatory)]
      $request,
      [bool]
      $returnHeader
   )
   try {
      if ($returnHeader) {
         Invoke-RestMethod @request -ResponseHeadersVariable rhv | Out-Null
         return $rhv
      }
      Invoke-RestMethod @request
   }
   catch [Microsoft.PowerShell.Commands.HttpResponseException] {
      $statuscode = $_.Exception.Response.StatusCode
      if ($statuscode -eq 'TooManyRequests') {
         if (!$request.ContainsKey('MaximumRetryCount')) {
            $request.Add('MaximumRetryCount', 3)
         }
         if ($returnHeader) {
            Invoke-RestMethod @request -ResponseHeadersVariable rhv | Out-Null
            return $rhv
         }
         Invoke-RestMethod @request
      }
      else {
         throw $_
      }
   }
}

Pass the request as a hashtable (Invoke-ResilientRestMethod $request), not with splatting.

HttpClient with the Web API

If you write your own Web API client, retry on 429 and read the header. Microsoft's sample uses Polly:

static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy(int maxRetries)
{
    return HttpPolicyExtensions
      .HandleTransientHttpError()
      .OrResult(r => r.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
      .WaitAndRetryAsync(
         retryCount: maxRetries,
         sleepDurationProvider: (count, response, context) =>
         {
            HttpResponseHeaders headers = response.Result.Headers;
            int seconds = headers.Contains("Retry-After")
               ? int.Parse(headers.GetValues("Retry-After").FirstOrDefault())
               : (int)Math.Pow(2, count);
            return TimeSpan.FromSeconds(seconds);
         },
         onRetryAsync: (_, _, _, _) => Task.CompletedTask);
}

Step 3: Set parallelism from x-ms-dop-hint and disable affinity

Parallel requests are the main lever for throughput, as long as you control them.

Use the recommended degree of parallelism. Dataverse returns an x-ms-dop-hint response header with the recommended degree of parallelism for the environment. Call WhoAmI once, read the header and use it as your maximum concurrency. With ServiceClient or CrmServiceClient, the same value is exposed as RecommendedDegreesOfParallelism:

serviceClient.EnableAffinityCookie = false;
 
var parallelOptions = new ParallelOptions
{
    MaxDegreeOfParallelism = serviceClient.RecommendedDegreesOfParallelism
};
 
await Parallel.ForEachAsync(entityList, parallelOptions, async (entity, token) =>
{
    ids.Add(await serviceClient.CreateAsync(entity, token));
});

Without this cap, .NET's default parallelism depends on the number of CPU cores on the client, which has nothing to do with what the environment can handle.

Disable server affinity. By default the service returns an affinity cookie that routes your requests to the same web server. Because limits apply per web server, turning affinity off spreads parallel requests across all eligible servers. Set EnableAffinityCookie = false on the SDK client, add <add key="PreferConnectionAffinity" value="false" /> to AppSettings in App.config, or set UseCookies = false on the HttpClientHandler when you use HttpClient.

Raise .NET Framework connection defaults. The default connection limit to a remote service is 2, so raise ServicePointManager.DefaultConnectionLimit to at least your planned concurrency. In .NET Core and later, HttpClientHandler.MaxConnectionsPerServer defaults to int.MaxValue.

Ramp up gradually. Microsoft recommends starting with a lower request rate and increasing it until you begin to receive limit errors, then letting Retry-After pace you. That keeps the retry-after duration short and total throughput high.

Step 4: Choose the right bulk pattern

Dataverse offers several ways to move many records. They interact with the limits differently.

PatternWhat it isEffect on limitsWhen to use
Single requests in parallelOne operation per request, concurrency capped at x-ms-dop-hintUses request budget; short execution time per requestMicrosoft's default recommendation for most scenarios
$batch (Web API) / ExecuteMultiple (SDK)Up to 1,000 requests in one HTTP callOne request towards the count; execution time of all operations accruesReducing payload when network latency matters; start with batches of about 10
Change sets / ExecuteTransactionAtomic group inside a batchSame as batchRelated records that must succeed or fail together
CreateMultiple, UpdateMultiple, UpsertMultipleBulk messages for records of one tableOne request; longer execution timeLarge loads to tables that support them; 100 to 1,000 records per request on standard tables
DeleteMultipleBulk deleteOne requestElastic tables only; use BulkDelete on standard tables

A few rules from the documentation:

  • Avoid large batches. Larger batches make you more likely to hit the execution time limit instead of the request limit. Start with 10 operations per batch and increase concurrency first.
  • Bulk messages on standard tables are all-or-nothing. Any error rolls back the entire request. Use them when you're confident most operations will succeed. On elastic tables partial success is possible, and Microsoft recommends 100 records per request.
  • Check availability. Not every standard table supports CreateMultiple and UpdateMultiple. Query sdkmessagefilters for the message and table before you build on it. A table that supports both also supports UpsertMultiple.
  • Keep the batch size configurable. Bulk requests can hit message size limits (the sandbox context limit is 116.85 MB when a plug-in is registered) and timeouts (ServiceClient defaults to four minutes). Design the job so you can send fewer records per request without a redeploy.

For error handling in $batch, add Prefer: odata.continue-on-error so one bad record doesn't stop the rest of the batch; the response then contains per-item errors.

Step 5: Tune Azure Data Factory copies into Dataverse

The Azure Data Factory and Synapse Dataverse connector (sink type DynamicsSink, DynamicsCrmSink or CommonDataServiceForAppsSink) uses upsert and exposes the settings that matter:

PropertyDefaultNotes
writeBatchSize10Rows written per batch
parallelCopies (copy activity)10 for the Dynamics sink10 x 10 means 100 records submitted concurrently by default
maxConcurrentConnectionsNot setUpper limit on concurrent connections; set it only when you want to cap
alternateKeyNameNot setAlternate key used for the upsert
ignoreNullValuesfalseWhen true, nulls in the source don't overwrite existing values
bypassBusinessLogicExecutionNot setCustomSync, CustomAsync or both; requires the prvBypassCustomBusinessLogic privilege
bypassPowerAutomateFlowsNot setBypasses Power Automate flows for the write

The connector documentation recommends keeping writeBatchSize at 10 or less to avoid throttling of concurrent calls, and describes the default 10 x 10 combination as the Dynamics service's recommendation. The best values depend on the table's column count, row size, and the plug-ins and workflows registered on it, so tune from the defaults rather than starting large.

A sink fragment with explicit values:

"sink": {
    "type": "DynamicsSink",
    "writeBehavior": "Upsert",
    "alternateKeyName": "contoso_externalid_key",
    "writeBatchSize": 10,
    "ignoreNullValues": true
}

The linked service should use AADServicePrincipal (Microsoft Entra service principal) or ManagedIdentity (user-assigned managed identity) authentication. The connector documentation notes that Office 365 authentication doesn't work when Conditional Access or multifactor authentication applies to the account.

Step 6: Reduce execution time on the server side

If you're hitting the execution time limit, the cost is often on the server, not in your client.

  • Synchronous plug-ins add their computation time to your request. Microsoft's guidance is to keep plug-in execution under 2 seconds and to prefer asynchronous registration where possible. The whole message operation, including synchronous plug-ins, must complete within 2 minutes.
  • Bypass custom logic for migrations. For one-off loads where downstream logic shouldn't fire, the ADF bypassBusinessLogicExecution property skips custom plug-ins and workflows (Microsoft-published ones still run), and SDK or Web API callers can send a bypass optional parameter such as BypassCustomPluginExecution, which needs its own bypass privilege. The ADF property requires the prvBypassCustomBusinessLogic privilege, which by default only the System Administrator role has.
  • Move from nightly bulk to near real time. Microsoft's guidance is that limits exist to smooth out large demand spikes. If a job processes a day's changes in minutes, consider event-driven synchronisation; see Dataverse integration choices: webhooks, Service Bus, plug-ins and flows.

Verification

After changing the client:

  1. Log x-ms-dop-hint at startup and confirm your maximum concurrency matches it.
  2. Log every 429 with its hex code and Retry-After value. A healthy high-throughput job sees occasional short Retry-After values; long or growing values mean you're retrying too aggressively.
  3. During a test load, record the two x-ms-ratelimit-* headers to see which budget runs out first. The target is a steady rate with few throttling errors, not zero errors at a much lower rate.

Troubleshooting

Number of concurrent requests exceeded the limit of 52. Your concurrency is above what one web server accepts for this user. Cap parallelism at x-ms-dop-hint, disable the affinity cookie, and in Azure Data Factory reduce writeBatchSize or parallelCopies, or set maxConcurrentConnections.

Combined execution time of incoming requests exceeded limit of 1,200,000 milliseconds... Requests are too expensive. Reduce batch or bulk sizes, review synchronous plug-ins and workflows on the target table, and simplify heavy queries.

Number of requests exceeded the limit of 6000 over time window of 300 seconds. Too many small calls from one identity. Combine operations with small batches or bulk messages, and confirm the job isn't running with affinity pinned to one server.

Throttling gets worse after adding retries. Retrying before Retry-After expires extends the penalty. Make sure every retry waits the full duration, and don't retry other 4xx errors in the same loop.

The request channel timed out attempting to send after 00:04:00. A bulk request took longer than the ServiceClient timeout. Send fewer records per request before you consider raising ServiceClient.MaxConnectionTimeout.

Different integrations throttle each other. If several jobs run as the same application user, they share one budget. Run unrelated integrations as separate application users; remember they still share the tenant's non-licensed request entitlement.

Checklist

  • Every client retries 429 and throttling faults after Retry-After.
  • SDK code uses ServiceClient or CrmServiceClient, not OrganizationServiceProxy.
  • Maximum concurrency comes from x-ms-dop-hint or RecommendedDegreesOfParallelism.
  • Affinity cookie disabled for parallel back-end clients.
  • Batches start small (about 10); bulk messages sized 100 to 1,000 on standard tables, 100 on elastic tables, configurable at run time.
  • Azure Data Factory sinks keep writeBatchSize at 10 or less unless testing proves otherwise.
  • Synchronous plug-ins on bulk-loaded tables reviewed for execution time.
  • Power Platform request entitlement tracked separately from service protection limits.

References

Questions people ask

What are the Dataverse service protection API limits?

Per user and per web server, Dataverse enforces 6,000 requests and 20 minutes (1,200 seconds) of combined execution time within a five-minute sliding window, plus a concurrent request limit of 52 or higher. Microsoft documents these as default values that can change and can vary between environments.

Do application users have higher Dataverse API limits?

No. Microsoft states that the same service protection limits apply to all users, including application users. Each authenticated user has its own independent limit, so separate integrations running as separate application users don't consume each other's budget.

Does using $batch or ExecuteMultiple avoid Dataverse throttling?

Only partly. A batch counts as one request towards the 6,000 request limit, but its operations add to the execution time limit, so large batches sent in parallel tend to hit the 20 minute execution time limit instead. Microsoft recommends starting with small batches of about 10 and raising concurrency until you get limit errors that you retry.

Are Dataverse service protection limits the same as Power Platform request limits?

No. Service protection limits are short-term (five-minute window, concurrency) and return 429 errors immediately. Power Platform request limits are 24-hour entitlements based on licences, and batch or bulk operations still count each create, update or delete towards them.

DataversePower PlatformWeb APIAzure Data Factory
  1. Power Platform ALM: ship apps and flows with managed solutions and pipelines

    Move Power Apps and Power Automate flows from development to test to production with managed solutions, environment variables, connection references and Power Platform pipelines.

    Architecture13 min read
  2. SharePoint lists vs Dataverse for Power Apps: delegation, limits, licensing

    Choose between SharePoint lists and Dataverse as the data source for a Power Apps solution by comparing delegation support, list limits, security, ALM and licensing requirements.

    Architecture11 min read
  3. Dataverse Application Users: Server-to-Server Auth with an Entra App

    Register a Microsoft Entra app, create a Dataverse application user with a least-privilege security role, get a client credentials token and call the Web API, then fix common errors.

    Architecture10 min read