Authorization Caching
How an authorization check is answered inside your own process, what the SDK holds to do it, and the window between changing someone's access and that change taking effect.
How a check is answered
A guarded route does not call Canopy. The SDK fetches two things and keeps them: where an identity holds each permission, and the shape of your hierarchy, and every check after that is a walk up the tree in your own memory. One call per identity per minute replaces one call per request, and your request path no longer waits on ours.
What is cached, and where
Two caches, split by what they depend on. Both live inside your process; Canopy stores nothing extra on your behalf.
A grant already means this node and everything beneath it, so the grant roots are never expanded into their descendants. That is what keeps the per-identity cache small even for an administrator granted at the top of a large tree, and it is why a check is a walk rather than a lookup: the SDK climbs from the node in question and asks whether any ancestor is one of the identity's grant roots. Measured, the hierarchy costs about 74 bytes per node once V8 has amortised the map: 3.6 MB for a 50,000-node tenant, against the 50–100 MB an idle Node process already holds. It is shared across every identity your server handles, so it does not grow with your user count.
The sixty-second window
An authorization change takes effect within 60 seconds: sometimes sooner, never later.
Which means the thing worth saying plainly: after you revoke someone's access, they can still act for up to a minute.
The window covers every change that can alter an answer:
Why permissions are not in the token
A reasonable question, especially coming from a provider that does it: why not put the permissions in the access token and skip the fetch entirely? Two things prevent it.
Canopy does emit an Application-wide permissions claim for OAuth clients that request the permissions scope, which is useful if you integrate with a stock OIDC library and only need the loose question answered. The SDK deliberately does not read it: two sources for one answer, with two different staleness windows, is worse than one.
Without the SDK
None of this is SDK-only. Both endpoints are ordinary HTTP, and the algorithm is short enough to write yourself:
GET /api/v1/identities/{id}/grants for the identity, and keep it for up to a minute.GET /api/v1/nodes once for the process, and keep it.If-None-Match carrying the ETag from that response. A 304 means your copy is still good; anything else means refetch.That is the entire mechanism. The SDK adds the caching, the revalidation timer and the failure handling, but nothing about it is privileged.
Serverless and edge
The design assumes a long-running process. On a serverless function or at the edge, every cold start begins with an empty cache and refetches both the grant roots and the tree. It still works, and it is still far better than a call per request, but the saving is smaller and it surfaces as slower cold starts rather than as steady-state latency.
Tuning
The window is yours to shorten, and there is one trap worth knowing before you do.
A window shorter than the gap between a user's requests saves nothing. Every request finds the cache expired and refetches, which is a call per request, the very thing the cache exists to avoid. Human-paced traffic has multi-second gaps between clicks, so a window of a few seconds can cost full price for no benefit.
If you need revocation faster than the window allows, that is not a tuning question. Revoke the identity's sessions as well: an access token that has been revoked stops working at its own expiry, and deactivating the identity denies it at the next refresh regardless of what any cache holds.
Tell us how we can improve this guide.