TLDR A common error after migrating an app from EWS to Microsoft Graph is “Application is over its MailboxConcurrency limit”. The number of concurrent connections allowed by Graph is 4 compared to EWS 27 so fixing this error may in some instances require you to redesign the way your application connects and threads.
Looking at the stats from my old blog one of the most widely accessed post is the one about the Mailbox Concurrency limit. This isn’t surprising given the big discrepancy between the concurrency limits between EWS and the Graph.
Why is the Mailbox Concurrency limit so low on a Graph compared to EWS?
Microsoft Graph isn’t always a more efficient API for accessing Exchange than EWS. In some scenarios, it can actually be less efficient due to the architectural layers Graph introduces, along with certain endpoint limitations—such as the lack of deep folder traversal.
That said, this really comes down to an engineering trade-off. Graph offers a modern, consistent API surface, but it doesn’t always map directly to the capabilities or efficiencies that EWS provided.
In practice, you’re left with a few options: work within the constraints, implement workarounds, redesign parts of your process, or consider alternative approaches depending on your requirements.
Make do with less
While that might sound a bit dramatic, most applications can successfully operate with fewer concurrent connections. This might slow down processing by a few seconds or minutes, but the impact is usually minimal.
Often, concurrency spikes are accidentally caused by modern design patterns like asynchronous lazy initialization. While great for performance, these patterns can trigger too many connections if synchronization isn’t handled properly.
In purely asynchronous environments, using an AsyncLock or a Semaphore ensures that only one worker can enter the initialization block at a time. In C#, SemaphoreSlim is the go-to choice, as it is specifically designed for lightweight, application-level async synchronization.
When you move from a single monolithic application to microservices, a local SemaphoreSlim is no longer enough. Since each microservice instance is unaware of the others, three instances with a local limit of 3 would hit the mailbox with 9 concurrent connections, instantly triggering Graph’s throttling.
To handle concurrency across a distributed architecture on approach is using something like
Distributed Locking (Redis)
Instead of a local semaphore, you use a distributed lock or counter. Redis is the one way of doing this. Before making a call to the Graph API, the microservice checks a key in Redis (e.g., mailbox_lock:user_id).
How it works: Use a library like RedLock.net. The service attempts to acquire one of the 4 available “slots” in Redis.
Pros: Real-time enforcement across all nodes.
Cons: Adds latency to every API call and introduces a dependency on Redis.
Batching in Microsoft Graph: The Good and the Trade-offs
Back in 2020, I wrote about an issue with batching in Microsoft Graph. At the time, if you submitted a batch with more than four requests without using the dependsOn property, the service would execute up to 20 requests concurrently and you would be usually throttled on the 16 out of 20 request.
The good news is that this behavior has since been improved. Microsoft Graph now caps concurrent execution within a batch to four concurrent requests, which aligns much better with backend service limits and avoids those widespread throttling errors.
However, this change introduces a different kind of challenge.
Today, the concurrency level is fixed—you can’t configure or tune how many requests within a batch are executed in parallel. While a limit of four is safe, it can also be restrictive depending on your application design.
For applications that rely heavily on batching, this effectively means that a single batch can consume all available concurrency for a mailbox. If your application needs to perform additional operations at the same time, those requests are more likely to hit concurrency limits and fail.
From a design perspective, having a fixed limit removes some flexibility. For example, if developers could reduce batch concurrency (say, to three), they could intentionally leave headroom for other operations to proceed in parallel. That kind of control can make a big difference in real-world, multi-threaded applications.
So while the updated behavior is a clear improvement over the earlier implementation, it also highlights a trade-off: improved safety and predictability, but less control for developers who need to fine-tune performance. It can lead to some pretty poor design decision and poor user experience. Where aren’t in the 1990’s writing single thread VB6 apps but the current engineering choice of 4 concurrent connections is.
In the end, batching in Microsoft Graph is much more reliable than it used to be—but it’s still something you need to design around carefully, especially when working close to service limits.
Conclusion
If you’re reading this after migrating an EWS application and are consistently running into these concurrency issues, there are a few key takeaways.
First, you need to make the most of the four concurrent connections available. Some patterns that worked well in EWS—such as frequent single-item binds or certain notification-driven approaches—may not be as efficient in Microsoft Graph. It’s worth revisiting these designs, looking at batching where appropriate, and taking the time to understand how batching actually behaves in practice. Also, be cautious when following generic guidance (including from LLMs) around features like dependsOn—always validate performance in your own workload.
Second, if your application relies on near real-time processing, consider whether introducing small delays is acceptable. In many cases, implementing backend queuing can help you sequence requests more effectively and stay within concurrency limits, rather than competing for them.
Ultimately, success with Graph often comes down to adapting your design to its constraints, rather than expecting a like-for-like transition from EWS.
For now, we wait patiently and hope the engineering gods eventually decide that four might not be the perfect number after all. And that a number greater this won’t break the universe or make it disintegrate in a puff of logic.

