Thank you for reaching out to Microsoft Q&A forum
Regarding your questions, I will provide answers based on my personal knowledge and official Microsoft documentation. Additionally, I’ll include reference links for your review so you can explore the details further.
1/ Why does Graph API sometimes return errors such as ApplicationThrottled or UnknownError even though the email is successfully delivered?
Based on my research, the reason because /sendMail is a fire‑and‑forget operation and most of the delivery pipeline runs after the API returns. This document states the behavior is: Graph builds a draft, saves it, returns 202 Accepted, and then Exchange transport picks up and delivers the message. Delivery is subject to Exchange limits/throttling and happens outside of the API call’s lifecycle.
In real tenants, you can see three related phenomena:
- Throttling or service busy conditions: Outlook service applies limits per app+mailbox. When you cross a limit, Graph can return
429(withApplicationThrottled) or503/504(ErrorServerBusy). These are expected signals to back off, not necessarily proofs the message wasn’t delivered, especially if you retried aggressively and one of the attempts succeeded as states in Microsoft Graph throttling guidance and Microsoft Graph service-specific throttling limits - Network/transient errors: The
UnknownError/ErrorInternalServerError(5xx) can occur even as the backend completes delivery. That said, if your client timed out, the server may have accepted the request anyway and finished later, so you can see an exception, but the recipient still gets the mail. - Ambiguous “202 + Retry‑After”: There have been cases where Graph returns 202 and a
Retry-Afterheader. Retrying the exact same request in this state can cause duplicate sends; many teams decided to not retry when status is 202 even if aRetry‑Afteris present as states in https://github.com/microsoftgraph/msgraph-sdk-java/issues/1487
2/ Is this expected behavior for the /sendMail endpoint?
My answer is Yes, the official docs state that a 202 means “accepted” but not “completed,” and delivery is subject to Exchange Online limitations and throttling. Most pipeline steps happen after the HTTP response so it’s also normal to see throttling/server busy responses under load.
3/ How should the client handle it, so exceptions don’t stop the rest of your process?
In my opinion, you can consider trying the approach below to see if it can resolve this problem:
- Set
http_errors => falseso you can branch on status codes and continue your business logic when appropriate. - If you receive 202 (with or without
Retry‑After), consider the request accepted and proceed with your business logic; record theclient-request-id/request-idfor diagnostics. Only retry on known transient statuses (see #4 below). - The Outlook service enforces mailbox concurrency (effectively ≤4 active ops per mailbox). If you see
ApplicationThrottledwithMailboxConcurrencyorCommandConcurrencyLimitReached, reduce parallel sends to the same mailbox and/or queue them.
4/ Recommended retry/backoff strategies specifically for /sendMail
- Retry only on:
429 TooManyRequests(ApplicationThrottled),503 ServiceUnavailable,504 GatewayTimeout. RespectRetry‑Afterprecisely when present. - Do not retry when:
- Status is 202 (even if
Retry‑Afterheader exists → known duplicate risk). - Validation/recipient faults (
400,ErrorInvalidRecipients) → fix the payload, don’t retry.
- Status is 202 (even if
- Limit parallelism to the same mailbox and overall
- For 503 specifically, some SDKs recommend creating a new connection when retrying. In Guzzle you can simulate this by sending
Connection: closeon the retry attempt**.**
5/ Do these errors indicate partial success, async backend errors, or server‑side throttling?
- ApplicationThrottled / CommandConcurrencyLimitReached mean Server-side throttling and you have to reduce concurrency and retry later.
- ErrorServerBusy / UnknownError / InternalServerError meanTransient backend issues; safe to retry with backoff.
- ErrorInvalidRecipients mean Permanent input error then fix payload.
Additionally, you can read here for more insight: https://stackoverflow.com/questions/19748105/handle-guzzle-exception-and-get-http-body
Note: Microsoft is providing this information as a convenience to you. These sites are not controlled by Microsoft, and Microsoft cannot make any representations regarding the quality, safety, or suitability of any software or information found there. Please ensure that you fully understand the risks before using any suggestions from the above link.
Hope my answer will help you, for any further concern, kindly let me know in the comment section
If the answer is helpful, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".
Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.