<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alex Mitu</title>
    <description>The latest articles on DEV Community by Alex Mitu (@alex_mitu_e264ef6e0d71197).</description>
    <link>https://dev.to/alex_mitu_e264ef6e0d71197</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4024381%2F65ef35c2-2e6f-42ab-a15d-ea410ce00aec.jpg</url>
      <title>DEV Community: Alex Mitu</title>
      <link>https://dev.to/alex_mitu_e264ef6e0d71197</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alex_mitu_e264ef6e0d71197"/>
    <language>en</language>
    <item>
      <title>The error said "Action failed." That was all it said.</title>
      <dc:creator>Alex Mitu</dc:creator>
      <pubDate>Wed, 22 Jul 2026 12:57:30 +0000</pubDate>
      <link>https://dev.to/alex_mitu_e264ef6e0d71197/the-error-said-action-failed-that-was-all-it-said-59ak</link>
      <guid>https://dev.to/alex_mitu_e264ef6e0d71197/the-error-said-action-failed-that-was-all-it-said-59ak</guid>
      <description>&lt;p&gt;A nightly sync job processes a few thousand records against a third party API. It runs fine for weeks. Then one night, one record throws &lt;code&gt;Operation failed&lt;/code&gt; and the job stops there. No stack trace, no field name, no hint about what actually went wrong. Every other record in the batch went through without a problem.&lt;/p&gt;

&lt;p&gt;If you've worked with any API that talks to a system you don't control, you've hit this exact wall. The error is real, the failure is real, and the message tells you almost nothing. This is not really about that one error string. It's about what to do when a vendor decides vague is safer than specific.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vague errors are usually on purpose
&lt;/h2&gt;

&lt;p&gt;It's tempting to assume the API author was just being lazy. Sometimes that's true. But a lot of the time, generic errors are a deliberate choice: security teams don't want to leak internal state through error messages, and support teams don't want users acting on assumptions that turn out to be wrong. &lt;code&gt;Action failed&lt;/code&gt; protects the vendor from a thousand support tickets built on a misread error code.&lt;/p&gt;

&lt;p&gt;That reasoning makes sense from where they're sitting. It doesn't make the debugging session any shorter from where you're sitting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step one: stop trusting the summary
&lt;/h2&gt;

&lt;p&gt;Most tools that call an API don't show you the raw response. They catch it, wrap it, and print something like &lt;code&gt;Job failed for item 4821&lt;/code&gt;. That wrapper is doing you a disservice the moment something unusual happens, because it's discarding the one thing you actually need: the body of the response the server sent back.&lt;/p&gt;

&lt;p&gt;Before doing anything else, find where the raw HTTP response is logged, or reproduce the call yourself directly against the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.example.com/v1/items/4821/sync"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; @item-4821-payload.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nine times out of ten the raw response has more in it than the tool ever showed you. A status code, a nested &lt;code&gt;error.details&lt;/code&gt; object, sometimes even the exact field that failed validation. The wrapper just never surfaced it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step two: isolate down to one unit
&lt;/h2&gt;

&lt;p&gt;If the failure is buried inside a batch job, don't try to debug the batch. Pull out the single item that's failing and run it on its own. This sounds obvious written down, but under time pressure it's the step people skip, because rerunning the whole job "just to see if it happens again" feels faster than isolating.&lt;/p&gt;

&lt;p&gt;It isn't faster. A batch run gives you noise: successful items, retries, unrelated warnings, timestamps that don't line up. A single isolated call gives you a clean before and after. Cut the batch out of the picture entirely and you can iterate in seconds instead of minutes.&lt;/p&gt;

&lt;p&gt;If you're working from logs instead of live access, the same principle applies. Grep for the specific identifier, not the general error string. Searching for &lt;code&gt;Operation failed&lt;/code&gt; gets you a wall of matches. Searching for the one ID that's actually broken gets you a narrow, readable slice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"4821"&lt;/span&gt; service.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"heartbeat"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step three: resolve the opaque ID before anything else
&lt;/h2&gt;

&lt;p&gt;A lot of "generic error, specific cause" situations turn out to be an identity problem in disguise. The ID in your log line is often internal, an integer, a GUID, a hash, something that means nothing to a human and nothing to the API's own documentation search. Before you theorize about causes, translate that ID into something you can actually reason about: a name, a path, a resource type.&lt;/p&gt;

&lt;p&gt;Most APIs that use opaque IDs also expose a lookup endpoint for exactly this purpose. It's easy to skip this step because it feels like a detour from the "real" debugging. It isn't. Half the time, once you know that ID 4821 is actually "a shared mailbox with no owner" or "a file with a name 400 characters long," the cause explains itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step four: write the raw response down, not your interpretation of it
&lt;/h2&gt;

&lt;p&gt;Once you get the real response back, resist the urge to summarize it in your own words right away. Copy the actual payload into your notes first. Interpretations drift. Two days later you'll remember "something about permissions" when the actual field said &lt;code&gt;insufficient_scope: files.write&lt;/code&gt;, and those are not the same debugging path.&lt;/p&gt;

&lt;p&gt;This matters even more if you're about to hand the case to someone else, or escalate it. "It's probably a permissions issue" gets deprioritized. A pasted error body with the exact scope name attached gets picked up fast, because the next person doesn't have to reproduce your work to trust it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually is
&lt;/h2&gt;

&lt;p&gt;None of this is specific to any one API or platform. It's the same four moves every time: stop trusting the wrapper, isolate to one unit, resolve the opaque identifier, and preserve the raw evidence before you translate it into a theory. The tools change. The order doesn't.&lt;/p&gt;

&lt;p&gt;The annoying part is that this process feels slow the first time you do it on a new system, and fast every time after. Once you know where a particular API hides its real error body, or which lookup endpoint turns an ID into a name, the next generic failure takes you five minutes instead of an afternoon. That's really the whole payoff: you're not getting better at guessing, you're building a small map of exactly where each system likes to hide the truth.&lt;/p&gt;

&lt;p&gt;So next time something throws you a wall of nothing, resist the urge to just retry it and hope. The information is almost always there. It's just sitting one layer below where the error message stopped looking.&lt;/p&gt;

</description>
      <category>api</category>
      <category>debugging</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Why Your EWS Impersonation Suddenly Stopped Working (And It's Probably Not Throttling)</title>
      <dc:creator>Alex Mitu</dc:creator>
      <pubDate>Fri, 10 Jul 2026 18:17:12 +0000</pubDate>
      <link>https://dev.to/alex_mitu_e264ef6e0d71197/why-your-ews-impersonation-suddenly-stopped-working-and-its-probably-not-throttling-2mjo</link>
      <guid>https://dev.to/alex_mitu_e264ef6e0d71197/why-your-ews-impersonation-suddenly-stopped-working-and-its-probably-not-throttling-2mjo</guid>
      <description>&lt;p&gt;Two months ago I picked up a ticket that looked routine: a job that reads mailbox data from Microsoft 365 through EWS, running fine for over a year, started failing on a subset of mailboxes in one tenant. Same app registration, same code path, same service account. The error in the logs pointed at throttling, so that's where the admin had already spent three days looking. Wrong direction. The actual cause had nothing to do with throttling budgets.&lt;/p&gt;

&lt;p&gt;This mix-up happens constantly right now, and it's worth understanding why, because the fix for one problem does nothing for the other, and chasing the wrong one wastes days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; if your EWS failures don't scale with request volume, stop tuning throttling and go check your Application Access Policy scope groups instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What EWS throttling actually looks like
&lt;/h2&gt;

&lt;p&gt;Exchange Online throttles EWS the same way it always has: budget-based. Every account gets a policy (the default is &lt;code&gt;EwsDefaultThrottlingPolicy&lt;/code&gt;, but plenty of tenants layer custom ones on top) that tracks a slowly-refilling budget rather than a simple call count. When you overspend it, you get back a 503 or 429 with an &lt;code&gt;X-MS-Diagnostics&lt;/code&gt; header telling you which budget got exhausted, usually the connection count or the concurrent-request limit.&lt;/p&gt;

&lt;p&gt;The tell for real throttling is consistency. It scales with load, correlates with concurrency and batch size, and clears up within minutes once you back off. If you graph failure rate against request volume, you'll see a clean relationship. If you double your batch size, failures increase. If you throttle yourself proactively (respecting &lt;code&gt;Retry-After&lt;/code&gt;, staying under EWSFindCountLimit for FindItem calls), it mostly goes away.&lt;/p&gt;

&lt;p&gt;That correlation is the whole diagnostic test. If your failures don't scale with volume, you're not looking at a throttling problem, no matter what the error message on the surface says.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed
&lt;/h2&gt;

&lt;p&gt;Over the past year or so, Microsoft tightened enforcement in two places that both produce errors easy to mistake for throttling.&lt;/p&gt;

&lt;p&gt;First, Application Access Policies. &lt;code&gt;New-ApplicationAccessPolicy&lt;/code&gt; has existed for years, letting admins scope which mailboxes an app registration can touch instead of granting it impersonation rights over the entire tenant. It used to be optional, something only security-conscious tenants bothered configuring. That's changed. More tenants are applying scoped access policies as a default hardening step, often as part of a broader Conditional Access or Secure Score push, and they're doing it without touching the app's actual permissions or the service account's RBAC role. The app still has &lt;code&gt;ApplicationImpersonation&lt;/code&gt;. It just can't reach mailboxes outside its assigned scope group anymore, and mailbox group membership drifts constantly as people get added, removed, or reassigned.&lt;/p&gt;

&lt;p&gt;Second, the long tail of Basic Auth deprecation. Most environments moved off Basic Auth for EWS years ago, but I still see tenants with a leftover service principal or a scheduled task somewhere using a cached token flow that quietly stopped working when Microsoft closed another enforcement gap. It's rarely the primary auth path failing outright. It's usually one forgotten integration that everyone assumed was already migrated.&lt;/p&gt;

&lt;p&gt;Both of these produce intermittent, mailbox-specific failures. Neither has anything to do with request volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Telling them apart
&lt;/h2&gt;

&lt;p&gt;Here's the sequence I actually use now before touching any throttling knobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Check whether failures correlate with specific mailboxes, not specific times.&lt;/strong&gt; Pull the failing mailbox list and check it against the policy's scope group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-ApplicationAccessPolicy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Format-List&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AppId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;PolicyScopeGroupId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AccessRight&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Get-DistributionGroupMember&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Identity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EWS-Scoped-Mailboxes"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;PrimarySmtpAddress&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare the mailbox list this returns against the mailboxes that are actually failing. Anything missing from the group is out of scope, and that's your answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Read the actual error code, not just the HTTP status.&lt;/strong&gt; These two failures look almost identical if your logging only captures the status code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;# Real throttling
&lt;/span&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;503&lt;/span&gt; &lt;span class="ne"&gt;Service Unavailable&lt;/span&gt;
&lt;span class="na"&gt;X-MS-Diagnostics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3006;reason="EWSMaxConcurrency budget exhausted";error_category="transient"&lt;/span&gt;

# Access policy scoping
HTTP/1.1 403 Forbidden
&amp;lt;S:Fault&amp;gt;
  &amp;lt;S:Code&amp;gt;&amp;lt;S:Value&amp;gt;Sender&amp;lt;/S:Value&amp;gt;&amp;lt;/S:Code&amp;gt;
  &amp;lt;S:Reason&amp;gt;&amp;lt;S:Text&amp;gt;ErrorAccessDenied&amp;lt;/S:Text&amp;gt;&amp;lt;/S:Reason&amp;gt;
&amp;lt;/S:Fault&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your logging layer only captures the HTTP status and not the response body, fix that first. You're throwing away the one piece of information that tells these two apart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Check the Microsoft 365 audit log around the date the failures started&lt;/strong&gt;, not the date they were noticed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Search-UnifiedAuditLog&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-StartDate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;-30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-EndDate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Get-Date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-Operations&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"New-ApplicationAccessPolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"Set-ApplicationAccessPolicy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"Remove-ApplicationAccessPolicy"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CreationDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;UserIds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Operations&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've found the actual change was made three weeks before anyone noticed, during a routine security review nobody connected to this app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Confirm the app's permission model.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-ManagementRoleAssignment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-RoleAssignee&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"svc-backup-app"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ApplicationImpersonation"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this returns a result, the app can impersonate any mailbox in the org by design, unless an access policy is actively narrowing that down. Worth knowing before you assume the access policy is even the right suspect.&lt;/p&gt;

&lt;p&gt;If none of that turns anything up and failures genuinely track with request volume and clear up after backing off, then fine, it's real throttling, go tune your batching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing the actual problem
&lt;/h2&gt;

&lt;p&gt;If it's an access policy issue, the fix is usually mechanical but tedious: audit the scope group, figure out why the mailbox fell out of it, and set up something that doesn't rely on someone remembering to update a static group every time a mailbox changes hands. A dynamic distribution group tied to whatever attribute actually determines "should this app reach this mailbox" beats a manually maintained list every time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;New-DynamicDistributionGroup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EWS-Scoped-Mailboxes"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-RecipientFilter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RecipientTypeDetails&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UserMailbox"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;CustomAttribute1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"backup-enabled"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point the access policy at this group instead of a static one. Membership now updates itself whenever &lt;code&gt;CustomAttribute1&lt;/code&gt; changes, instead of depending on someone remembering to edit a list.&lt;/p&gt;

&lt;p&gt;The bigger fix, though, is getting off tenant-wide impersonation entirely. Microsoft has been pushing RBAC for Applications as the replacement, and it's worth the migration even outside of this specific problem. Instead of one service account impersonating anyone, you assign granular, resource-scoped permissions directly to the app registration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;New-ServicePrincipal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AppId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;your-app-id&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ObjectId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;object-id&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-DisplayName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Backup-App"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;New-ManagementScope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Backup-App-Scope"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-RecipientRestrictionFilter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;CustomAttribute1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"backup-enabled"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;New-ManagementRoleAssignment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Application Mail.Read"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-App&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;your-app-id&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-CustomRecipientWriteScope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Backup-App-Scope"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This replaces a single tenant-wide &lt;code&gt;ApplicationImpersonation&lt;/code&gt; grant with a role scoped to exactly the mailboxes the management scope defines. Exact cmdlet parameters shift a bit between Exchange Online PowerShell module versions, so treat this as the shape of the migration and confirm current syntax against Microsoft's docs before running it against production. It's more setup work up front, but it means you stop being exposed to "someone tightened a policy somewhere and now half your mailboxes silently fail" as a category of incident. Given that EWS itself is on Microsoft's retirement roadmap in favor of Graph API, this is also the direction you'd be migrating toward anyway. Doing the permission model migration first, before the API migration, splits the work into two smaller, less risky changes instead of one large one.&lt;/p&gt;

&lt;p&gt;One more thing worth doing regardless: separate your retry logic by error type. A lot of applications treat every failed EWS call the same way and retry with exponential backoff, which is correct for a 503 throttling response and actively harmful for a 403 access-denied response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;should_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_code&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;error_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ErrorServerBusy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;   &lt;span class="c1"&gt;# transient, back off and retry
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;error_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ErrorAccessDenied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;  &lt;span class="c1"&gt;# a permission problem, retrying won't fix it
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Retrying an access-denied error just burns your throttling budget on calls that were never going to succeed, and can end up creating the throttling problem you originally thought you had.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern underneath this
&lt;/h2&gt;

&lt;p&gt;None of this is exotic. It's the same lesson as always: a generic error handler will happily lump together two unrelated failure modes under one log line, and the fix for each one does nothing for the other. The throttling ticket that isn't actually about throttling has become common enough in the last year that I've started assuming access policy scope first and traffic volume second, and I've been right more often than not.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>cloud</category>
      <category>tutorial</category>
      <category>security</category>
    </item>
  </channel>
</rss>
