Engineering Sub-10ms Authorization Checks
Authorization runs on the critical path of every request, so its p99 latency is your p99 latency. Four techniques — colocating the PDP, denormalizing decision data, request-scoped caching, and batching — for keeping access checks under 10ms.
Authorization runs on the critical path of every single request. That means your authorization p99 is a floor under your entire API’s p99 — if an access check takes 80ms, no endpoint can be faster than 80ms. Getting checks under 10ms is not a vanity metric; it is a prerequisite for calling authorization on every request the way zero trust demands.
Where the Milliseconds Go
- Network hops to a distant PDP or database.
- Joins / fan-out — fetching a policy, then roles, then permissions in sequence.
- Attribute fetches in ABAC, from slow Policy Information Points.
- Cold starts on serverless PDPs.
Technique 1: Colocate the PDP
Every network hop between your service and the decision point adds round-trip latency. Run the PDP in the same region — ideally the same VPC — as your application. A cross-continent hop alone can cost 100–150ms; same-region is sub-millisecond.
Technique 2: Denormalize the Decision Data
The RBAC evaluation path is naturally a graph (subject → roles → permissions). Sequential lookups multiply latency. Store the data so a decision needs the fewest possible round trips — this is exactly why we chose single-table DynamoDB design, which keeps all of a tenant’s data in one partition and resolves a check in three operations against the same node.
Technique 3: Request-Scoped Caching
A single request often checks the same permission several times (render, then mutate). Deduplicate within the request lifecycle so you pay for the decision once:
import { cache } from 'react'
// Deduplicated per request — same args resolve to one PDP call.
export const checkPermission = cache(async (userId, scopeId, resource, action) => {
const { allowed } = await pdp.hasAccess({ subjectId: userId, scopeId, resource, action })
return allowed
})Technique 4: Batch Checks
Rendering a dashboard often needs many decisions at once. Batch them into one call instead of N sequential round trips:
const decisions = await pdp.batchHasAccess([
{ subjectId, scopeId, resource: 'invoice', action: 'create' },
{ subjectId, scopeId, resource: 'invoice', action: 'delete' },
{ subjectId, scopeId, resource: 'report', action: 'export' },
])Measure, Don’t Guess
const start = performance.now()
const { allowed } = await pdp.hasAccess(req)
metrics.histogram('authz.latency_ms', performance.now() - start)Track p50/p95/p99 separately. The tail is what your users feel.
How WardenAuth Keeps Checks Fast
WardenAuth evaluates a decision in memory in microseconds; a live check is dominated by the network and the data read, typically tens of milliseconds from an application in the same AWS region. Batch checks, cache decisions per request, and keep your app close to us-east-1 to stay on the fast side. See it for yourself.