What this usually means
429 signals throttling; 503 indicates service unavailability. Neither status identifies the cause of a wider Microsoft 365 incident. Microsoft documents backoff for both. Start by classifying the operation: a safe GET can usually be retried without changing remote state, but an uncertain mail-send or other write requires endpoint-specific reasoning before another attempt.
The first ten minutes — establish facts before touching code.
- 1Capture the method, redacted route template, response status, Retry-After value, elapsed time and attempt number.
- 2Check whether the Graph SDK or HTTP middleware already retries, and nominate one layer to own the budget.
- 3Inspect the delay passed to the timer: Retry-After numeric values are seconds, while many timers use milliseconds.
- 4Reconstruct the original operation deadline across every attempt and sleep, including cancellation from the caller.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchThe HTTP response headers and documented retry middleware configuration for the SDK version in use.
- searchApplication concurrency controls, polling schedules and worker queue admission rates.
- searchRedacted request-id or client-request-id correlation records, with UTC timestamps and retry timing.
Practical causes, not theory. These are the things you will actually find.
- warningA unit mismatch schedules a 10-second server delay as 10 milliseconds.
- warningNested policies multiply attempts, or many workers use the same backoff schedule without jitter.
- warningEach retry resets its timeout, so total work continues after the original caller deadline.
- warningA broad retry predicate treats authentication, permission and validation errors as transient capacity problems.
Concrete fix directions. Pick the one that matches your root cause.
- buildFor safe reads, honour a valid Retry-After and stop if the required wait cannot fit inside the remaining operation budget.
- buildWhen no usable server delay exists, use capped exponential backoff with jitter, a maximum attempt count and a total elapsed-time limit.
- buildReduce concurrency or polling pressure alongside backoff; retries cannot repair demand that continuously exceeds capacity.
- buildHandle writes using the specific endpoint contract and an application operation ledger; an arbitrary header does not make Graph writes idempotent.
A fix you cannot prove is a guess. Close the loop.
- verifiedUse a fake clock and scripted responses to check numeric, HTTP-date, absent, malformed and already-past Retry-After values.
- verifiedAssert that no next attempt starts before a valid required delay, and that cancellation interrupts pending waits.
- verifiedTest a delay longer than the remaining budget: the operation should stop rather than shorten the server wait.
- verifiedRun a bounded concurrency test and confirm one logical request cannot expand into uncontrolled nested attempts.
Things that make this bug worse or harder to find.
- warningDo not infer a provider outage from a single 429 or label every 503 as throttling.
- warningDo not reset the overall deadline, ignore cancellation, or retry forever because the service might recover.
- warningDo not log access tokens, Authorization headers, mailbox addresses, response bodies or complete URLs containing sensitive query values.
HTTP allows Retry-After as either a non-negative integer number of seconds or an HTTP date. For a numeric value of 10, a millisecond timer needs a 10,000-millisecond wait. Reject malformed values deliberately rather than letting a permissive parser accept a prefix such as 10oops. Keep the original response value available in scrubbed diagnostics.
For an HTTP date, calculate the interval from the current wall clock, with zero as the minimum for a past date. Keep clocks synchronized and treat an unparseable date as unusable input. Use a monotonic clock for the overall elapsed-time budget so a wall-clock adjustment does not grant extra retry time. Add jitter without making the retry earlier than a valid server-directed wait.
Imagine a read with 20 seconds left and a response asking you to wait 30 seconds. There is no valid in-budget retry. Return a controlled failure or defer the work according to the product contract; do not clamp the wait down to 20 seconds and then make another request. Maximum attempts and total time solve different problems, so enforce both.
Check all participating layers. Three attempts in an SDK wrapped by three application attempts can produce nine network calls. Polling, job redelivery and load-balancer retries may add more. Make the ownership explicit, retain the original deadline through queue handoffs, and distinguish a deferred operation from an immediate retry.
A GET normally has no requested state-changing effect, making bounded retries a reasonable default. Sending email is different: a timeout can occur after the remote service accepted the request. Repeating that POST may send another message. Graph does not offer a universal idempotency guarantee simply because a client supplies an idempotency-key header.
Use documented endpoint capabilities where they exist. Otherwise retain a stable application operation identity and reconcile uncertain results before resending. The linked email recovery guide follows that boundary. To practise the timing mistake safely, use Buglyst’s simulated Graph retry lab; it is a normal free account lab, not a connection to your Microsoft tenant.
Related debugging guides
Frequently asked questions
Will the Graph SDK handle all retries for me?
Graph SDKs include retry handlers, but defaults and configuration depend on the SDK and version. Check the active middleware and align it with your caller deadline. An outer retry loop can still multiply requests.
Should every 503 trigger another request?
Only when the operation can be retried safely and the retry fits its policy. Use backoff, a total deadline and cancellation. A write with an uncertain outcome may need reconciliation rather than automatic replay.