Microsoft Graph is one of those APIs that's easy to demo and surprisingly fiddly to operate. The Quickstart will get you a token in eight lines, and for a weekend project that's the end of the story. Getting Graph to behave in production — under load, on an edge runtime, with rotating credentials and error handling you'd trust with a client's email — is a different exercise entirely.

We run Graph from an edge Worker runtime in production: it sends real invoices, real help-desk notifications, real client email for the businesses we support. These are the field notes I wish someone had handed me at the start — including the parts the documentation genuinely leaves out.

Start with certificate auth, not client secrets

The first fork in the road is how your app proves it's your app, and the convenient path is the wrong one.

Client secrets are convenient and, in my view, indefensible for anything that matters. They live in environment variables. They get pasted into Slack messages during debugging sessions. They get rotated reluctantly, usually right after an incident rather than right before one. Every one of those is a normal Tuesday at a normal company — which is exactly the problem.

Certificate-based credentials cost about ninety minutes to set up once, and they remove an entire category of incident from your future. The private key never travels; you sign a short-lived assertion instead of sending a durable secret. Here's the shape of it:

const jwt = await new SignJWT({ aud: tokenEndpoint, iss: clientId, sub: clientId, jti: crypto.randomUUID() })
  .setProtectedHeader({ alg: 'RS256', x5t: thumbprintBase64Url })
  .setIssuedAt()
  .setExpirationTime('5m')
  .sign(privateKey);

Ninety minutes, once. Cheap insurance.

That callout is not hypothetical. The error message gives you nothing; the fix is one encoding call. The gap between those two sentences is why this post exists.

Cache the token — but respect the runtime

A naive implementation acquires a fresh token on every request. Graph will tolerate this for a while, then quietly start throttling you, and the throttling will look like random flakiness rather than a clear signal. So: cache the access token.

But here's where the edge runtime bites. On a Worker-style platform, module-level state does not reliably survive — isolates get recycled whenever the platform feels like it. A module-level token cache will appear to work in testing, then produce bizarre intermittent failures in production as some requests hit a warm isolate and some don't.

On an edge runtime, a cache that "usually works" is worse than no cache at all — it converts a predictable cost into an unpredictable bug.

The discipline is to cache at the scope your runtime actually guarantees — request scope on Workers — and treat any longer-lived reuse as an optimization you verify, not an assumption you inherit from Node habits.

Handle 429 like an adult

Sooner or later Graph will send you a 429 Too Many Requests, and how you respond says a lot about how your next hour goes.

A 429 from Graph is not an error to be retried. It's feedback about the tenant. The rate limit you just hit isn't scoped to the one unlucky call — so the right response is to back off every Graph call for the duration of the Retry-After header, not just the request that tripped the limit. Retry the single call and you'll spend the next sixty seconds rediscovering the same limit endpoint by endpoint, multiplying the very pressure that caused the throttle.

Practically, that means a shared backoff gate: one signal, honored by all Graph traffic, cleared when Retry-After expires. It's a few lines of code and the difference between a graceful pause and a self-inflicted outage.


The short version

If you're about to put Microsoft Graph into production on an edge runtime, here's the checklist I'd hand you:

  • Certificates over secrets — ninety minutes now, or an incident review later.
  • Base64url SHA-1 of the DER cert for x5t — write it on a sticky note.
  • Cache tokens at request scope — module-level caches lie on Workers.
  • Honor Retry-After globally — a 429 is about the tenant, not the call.

None of this is exotic. All of it is the difference between an integration you demo and one you operate. Graph rewards the boring disciplines — which, honestly, is true of most infrastructure worth having.