Graph API sendMail succeeds but Guzzle throws RequestException with errors like ErrorInternalServerError, ApplicationThrottled, CommandConcurrencyLimitReached

IT development 0 Reputation points
2025-11-20T07:50:08.0033333+00:00

I am using Microsoft Graph API /sendMail endpoint from a PHP application (using Guzzle HTTP client).

The mail is successfully delivered to the recipient, but the Graph API request still ends in a RequestException, which prevents my remaining business logic from executing.

These are some of the errors returned in the exception response body:

  • ErrorInvalidRecipients
  • UnknownError
  • ErrorServerBusy
  • ApplicationThrottled
  • ErrorInternalServerError
  • CommandConcurrencyLimitReached

For some of these (e.g., UnknownError, ApplicationThrottled, CommandConcurrencyLimitReached), the email is still sent, but Graph API returns an error instead of the usual 202 Accepted.

Because Guzzle catches it as an exception, the rest of my code fails to run.

Below is the code used for sending mail:

try {

$gapiAccessToken = null;

$gapiClient = null;

AccessToken($gapiAccessToken, $gapiClient, $companyId);

$requestBody = json_encode($emailData);

$response = $gapiClient->post(

    'https://graph.microsoft.com/v1.0/users/' . _USER_ . '/sendMail',

    [

        'headers' => [

            'Authorization' => 'Bearer ' . $gapiAccessToken->getToken(),

            'Content-Type'  => 'application/json'

        ],

        'body' => $requestBody

    ]

);

$statusCode = $response->getStatusCode();

if ($statusCode === 202) {

    unset($emailData["message"]["body"]);

    unset($emailData["message"]["attachments"]);

    $requestLog = json_encode($emailData);

    // logging removed for brevity

    return ['success' => true, 'status' => $statusCode];

}

}

catch (RequestException $e) {

$errorBody = $e->hasResponse()

    ? $e->getResponse()->getBody()->getContents()

    : 'No response body';

// log error

return ['success' => false, 'error' => $errorBody];

}

catch (Exception $e) {

// log generic error

return ['success' => false, 'error' => $e->getMessage()];

}

My Questions

1.Why does Graph API sometimes return errors such as ApplicationThrottled or UnknownError even though the email is successfully delivered?

2.Is this expected behavior for the /sendMail endpoint?

3.In these cases, how should the client application handle the situation so that exceptions do not stop the rest of the process?

4.Are there recommended retry/backoff strategies specifically for /sendMail when errors are returned even after successful delivery?

5.Do these errors indicate partial success, asynchronous backend errors, or server-side throttling?

Any guidance from Microsoft or community experts would be helpful.

Microsoft 365 and Office | Development | Other
0 comments No comments
{count} votes

1 answer

Sort by: Most helpful
  1. Steven-N 15,720 Reputation points Microsoft External Staff Moderator
    2025-11-20T09:56:57.0133333+00:00

    Hi IT development

    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 (with ApplicationThrottled) or 503/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-After header. Retrying the exact same request in this state can cause duplicate sends; many teams decided to not retry when status is 202 even if a Retry‑After is 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:

    1. Set http_errors => false so you can branch on status codes and continue your business logic when appropriate.
    2. If you receive 202 (with or without Retry‑After), consider the request accepted and proceed with your business logic; record the client-request-id/request-id for diagnostics. Only retry on known transient statuses (see #4 below).
    3. The Outlook service enforces mailbox concurrency (effectively ≤4 active ops per mailbox). If you see ApplicationThrottled with MailboxConcurrency or CommandConcurrencyLimitReached, reduce parallel sends to the same mailbox and/or queue them.

    4/ Recommended retry/backoff strategies specifically for /sendMail

    1. Retry only on: 429 TooManyRequests (ApplicationThrottled), 503 ServiceUnavailable, 504 GatewayTimeout. Respect Retry‑After precisely when present.
    2. Do not retry when:
      • Status is 202 (even if Retry‑After header exists → known duplicate risk).
      • Validation/recipient faults (400, ErrorInvalidRecipients) → fix the payload, don’t retry.
    3. Limit parallelism to the same mailbox and overall
    4. For 503 specifically, some SDKs recommend creating a new connection when retrying. In Guzzle you can simulate this by sending Connection: close on the retry attempt**.**

    5/ Do these errors indicate partial success, async backend errors, or server‑side throttling?

    1. ApplicationThrottled / CommandConcurrencyLimitReached mean Server-side throttling and you have to reduce concurrency and retry later.
    2. ErrorServerBusy / UnknownError / InternalServerError meanTransient backend issues; safe to retry with backoff.
    3. 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.


Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.