Work · 02

Outlook calendar sync at enterprise scale, without loops or notification storms

Outlook calendars and the platform stayed correctly in sync for thousands of users, without duplicate notifications and without manual fixes.

Integrations · Microsoft 365

2-wayOutlook sync at enterprise scale, without loops or storms
Context
Two-way sync between the platform and Outlook on Microsoft Graph: webhooks, subscriptions, delta sync, Outlook add-in, Teams app, Azure AD attribute sync. At enterprise scale, every mistake becomes a sync loop or a notification storm.
What we did
We took over the whole integration surface. Loops and duplicates were stopped with strict change keys, ETags and short-lived idempotency values in Redis. Webhook storms are collected in a bucket and processed once they end. All Graph work goes through queues, with per-tenant token caching, and subscriptions are renewed by cron a day before they expire, with retries and an alert when a renewal keeps failing.
Result
A stable integration enterprise customers rely on every day, owned end to end and improved continuously over several years in production.
Stack
Microsoft Graph, Azure AD, Teams, PHP, Symfony Messenger

Three ways two-way sync goes wrong

Keeping two calendars in step sounds simple until both sides can write. Then it fails in three recognizable ways: duplicates, loops and floods of notifications. We have dealt with all three, on Microsoft Graph, at enterprise scale.

Breaking the loop

A loop starts when our own write comes back as news. We write an event to Outlook, Graph fires a webhook for that write, and a naive integration treats the webhook as a fresh change and writes again.

The fix is to recognize an echo. We compare strict change keys and ETags to tell a real change from the reflection of our own, and we keep short-lived idempotency values in Redis so the same change is never applied twice. The same mechanism keeps duplicates out.

Waiting out the storm

Graph is generous with notifications. A single simple edit can arrive as five or six webhook calls, and attendee changes or bulk edits multiply that. Reacting to each one means doing the same work six times and notifying people six times.

So notifications are not processed as they arrive. They are collected in a bucket, and the bucket is processed once the storm has ended.

Rate limits, and the one time we hit one

All Graph work goes through queues, with tokens cached per tenant. In normal operation we have not run into Graph throttling.

The one time Graph did limit us, request volume was not the cause. The problem was on the outbound side of our own network: SNAT ports. Raising the allocated outbound ports fixed it. It is a useful reminder that a rate-limit error is not always about your request rate.

Subscriptions that renew themselves

Graph webhook subscriptions expire, and an expired subscription fails quietly: changes simply stop arriving. Cron jobs renew every subscription a day before it expires and retry each failure. If a renewal keeps failing, an alert tells us.

Where people already work

The Outlook add-in, the Teams app and the Azure AD attribute sync share one idea: nobody should have to open a second system. If a team lives in Teams, the booking happens in Teams. If they live in Outlook, it happens in Outlook. User attributes follow Azure AD, so people are not maintained in two places.

Never finished

The integration has been in production for several years, with monitoring and alerts on the failure modes that recur. It is not a project that ended. There is always a small improvement to make, and clear ownership of the whole surface means it gets made.

Contact

Let's talk

Tell us what you have, in your own words. You do not need to know which technology the system runs on; we will work that out. You get back questions and a written proposal.

Write to usor directly at [email protected]