<?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>How to Actually Read Linux Logs When Something's Broken</title>
      <dc:creator>Alex Mitu</dc:creator>
      <pubDate>Wed, 12 Aug 2026 14:51:08 +0000</pubDate>
      <link>https://dev.to/alex_mitu_e264ef6e0d71197/how-to-actually-read-linux-logs-when-somethings-broken-2mhb</link>
      <guid>https://dev.to/alex_mitu_e264ef6e0d71197/how-to-actually-read-linux-logs-when-somethings-broken-2mhb</guid>
      <description>&lt;p&gt;It's 3am, a service is down, and you have exactly one clue: something logged an error somewhere. This is the part of the job nobody teaches directly. You pick it up by staring at terminals for a few years, usually while half asleep, usually under pressure. Here's the version of that knowledge I wish someone had handed me earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know where to actually look first
&lt;/h2&gt;

&lt;p&gt;Most people's first instinct is &lt;code&gt;cat /var/log/syslog&lt;/code&gt; and scroll. That works, sort of, right up until the log file is 400MB and the failure happened three services ago. Before you dive in, figure out which of the two logging worlds you're in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;journald&lt;/strong&gt; is what most modern distros use by default. Logs are binary, structured, and queried through &lt;code&gt;journalctl&lt;/code&gt;, not read directly as text files.&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="c"&gt;# Everything from a specific service, most recent first&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; nginx.service &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="c"&gt;# Only the last 20 minutes&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; nginx.service &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"20 minutes ago"&lt;/span&gt;

&lt;span class="c"&gt;# Only errors and worse (crit, alert, emerg)&lt;/span&gt;
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; nginx.service &lt;span class="nt"&gt;-p&lt;/span&gt; err
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Plain text logs&lt;/strong&gt; under &lt;code&gt;/var/log&lt;/code&gt; are still very much alive, especially for anything that predates systemd conventions or writes its own log files by choice, think nginx, postgres, most application-level logging.&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;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/nginx/error.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mistake people make here isn't picking the wrong tool, it's not checking which one applies before they start searching. If you grep a file that's empty because the service actually logs to journald, you'll conclude "no errors" when really you were just looking in the wrong place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual diving technique
&lt;/h2&gt;

&lt;p&gt;Once you know where the logs live, the skill isn't reading faster. It's narrowing faster. A few things that consistently save time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time-box before you grep.&lt;/strong&gt; Don't search the whole file for a pattern. Get the failure window first, even roughly, then constrain the search to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myapp.service &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"14:00"&lt;/span&gt; &lt;span class="nt"&gt;--until&lt;/span&gt; &lt;span class="s2"&gt;"14:15"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Search around a match, not just for it.&lt;/strong&gt; The line with the word "error" is rarely the whole story. The line before it usually explains why, and the line after usually shows what happened as a result.&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;-B&lt;/span&gt; 5 &lt;span class="nt"&gt;-A&lt;/span&gt; 10 &lt;span class="s2"&gt;"connection refused"&lt;/span&gt; app.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Follow one identifier through the whole log, not the whole error message.&lt;/strong&gt; If a request or a job has an ID, chase that ID specifically. Searching for the generic error string gets you a wall of matches from every unrelated failure that shares the same wording. Searching for the specific ID gets you the exact sequence of events for the thing you actually care about.&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="s2"&gt;"req-8f21a3"&lt;/span&gt; app.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;If the logs are structured JSON, stop treating them like plain text.&lt;/strong&gt; A lot of modern services log JSON lines instead of free text, and grepping those by eye is painful. &lt;code&gt;jq&lt;/code&gt; turns that into something readable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; myapp.service &lt;span class="nt"&gt;-o&lt;/span&gt; json | jq &lt;span class="s1"&gt;'select(.PRIORITY == "3")'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of this is exotic. It's the difference between opening a 50,000 line file and reading top to bottom, versus asking three narrow questions in sequence: when did it start, what surrounded the failure, and what specific thing was involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common errors, decoded by service
&lt;/h2&gt;

&lt;p&gt;This part won't be exhaustive, nothing is, but these are the ones that show up constantly and get misread constantly.&lt;/p&gt;

&lt;h3&gt;
  
  
  nginx
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Message&lt;/th&gt;
&lt;th&gt;What it usually means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;502 Bad Gateway&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The upstream (your app server) crashed, restarted, or refused the connection. nginx is fine, whatever's behind it isn't.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;504 Gateway Timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The upstream is alive but too slow to respond within nginx's timeout. Different problem than 502, don't treat them the same.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;connect() failed (111: Connection refused)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nothing is listening on the port nginx is trying to reach. Check if the upstream process is even running.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;upstream sent too big header&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The app is returning headers nginx wasn't configured to expect. Usually shows up after adding auth tokens or cookies without bumping &lt;code&gt;proxy_buffer_size&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  PostgreSQL
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Message&lt;/th&gt;
&lt;th&gt;What it usually means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FATAL: too many connections for role&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Connection pool exhaustion. Either the app isn't closing connections properly, or &lt;code&gt;max_connections&lt;/code&gt; genuinely needs raising.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deadlock detected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Two transactions grabbed locks in opposite order. Postgres killed one to break the cycle. Look at the accompanying detail line, it names both queries.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;canceling statement due to statement timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not a bug by itself, it's a configured limit doing its job. The real question is why that query needed longer than expected this time.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;could not extend file... No space left on device&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exactly what it says, but people still spend twenty minutes assuming it's a config problem before checking disk space.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  systemd / journald
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Message&lt;/th&gt;
&lt;th&gt;What it usually means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Failed with result 'exit-code'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The process exited with a nonzero code. Check &lt;code&gt;systemctl status &amp;lt;service&amp;gt;&lt;/code&gt; right after, it usually shows the actual exit code, which tells you far more than the generic message.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Failed with result 'oom-kill'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The kernel killed the process for using too much memory. This is a memory problem, not a crash to debug in the app logs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Start request repeated too quickly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Systemd gave up restarting the service because it kept failing immediately. This message is a symptom, the real error is earlier in the same unit's log, right before the first failed start.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What this actually comes down to
&lt;/h2&gt;

&lt;p&gt;None of the tricks above are about memorizing every possible error string. That list is infinite and you'll never finish it. What actually matters is the order of operations: know which logging system you're dealing with before you search, narrow to a time window before you grep, follow a specific identifier instead of a generic phrase, and read the lines around a match instead of just the match itself.&lt;/p&gt;

&lt;p&gt;The error message rarely lies. It's just usually incomplete on its own, and the missing part is almost always sitting a few lines away, waiting for you to widen the search by five lines in either direction.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>devops</category>
      <category>debugging</category>
      <category>tutorial</category>
    </item>
    <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>
