WardenAuthAgent Security
PricingDocsCompareBlogLearnChangelog
Sign inGet started free
All posts
Architecture
July 19, 2026
11 min read

Authorization Caching Patterns: Speed Without Staleness

Caching authorization decisions makes your API fast but risks serving stale permissions. Request-scoped vs cross-request caching, TTL strategies, cache invalidation on role changes, and the patterns that keep authorization both fast and correct.


Authorization runs on the critical path of every request. Cache it, and your API is fast. Cache it wrong, and a revoked role takes minutes — or hours — to take effect. This post covers the caching strategies that keep authorization both fast and correct: request-scoped deduplication, short-TTL cross-request caches, event-driven invalidation, and the one pattern you should never use.

Why cache authorization at all?

The PDP call — fetching a user's roles, resolving permissions, evaluating conditions — can take 3-10ms even in a well-designed system. When a single request checks the same permission multiple times (render an invoice list, then check delete on each row), the latency compounds. A cache that eliminates duplicate PDP calls within a request can save 20-50ms — the difference between a snappy API and one that feels slow.

Pattern 1: Request-scoped cache (always safe)

Request-scoped caching deduplicates PDP calls within a single HTTP request. It is always safe because the cache lives as long as the request — typically under 100ms. No permission change can happen within that window, so there is no staleness risk.

typescript
import { cache } from 'react'

// React's cache() deduplicates calls within a single request render
const checkPermission = cache(async (userId, scopeId, resource, action) => {
  const { allowed } = await pdp.hasAccess({ subjectId: userId, scopeId, resource, action })
  return allowed
})

// Multiple calls to checkPermission with same args — only one PDP call
await checkPermission(userId, scopeId, 'invoice', 'read')  // PDP call
await checkPermission(userId, scopeId, 'invoice', 'read')  // cached
await checkPermission(userId, scopeId, 'invoice', 'delete') // different args → PDP call

Pattern 2: Short-TTL cross-request cache (use with care)

Cross-request caching shares PDP decisions across multiple requests — typically via an in-memory cache (Redis, Memcached, or an in-process LRU). The tradeoff: cache hits save latency, but a stale cache means a revoked permission is still "allowed" until the TTL expires or the cache is invalidated.

Never cache authorization decisions for security-critical actions: payments, deletes, permission changes, user invite/removal. If you must cache, keep TTLs short (5-30 seconds) and invalidate on any role or permission change.
typescript
// Safe cross-request cache: short TTL + skip-list for sensitive actions
const DANGEROUS_ACTIONS = ['delete', 'approve', 'revoke', 'transfer']
const CACHE_TTL = 30_000 // 30 seconds

async function checkAccess(req: AccessRequest): Promise<boolean> {
  // Never cache security-critical actions
  if (DANGEROUS_ACTIONS.includes(req.action)) {
    return (await pdp.hasAccess(req)).allowed
  }

  const cacheKey = `authz:${req.subjectId}:${req.scopeId}:${req.resource}:${req.action}`
  const cached = await cache.get(cacheKey)
  if (cached !== null) return cached

  const { allowed } = await pdp.hasAccess(req)
  await cache.set(cacheKey, allowed, { ttl: CACHE_TTL })
  return allowed
}

Pattern 3: Event-driven invalidation (correct but complex)

Instead of relying on TTL expiration, invalidate cached decisions when the underlying data changes. When a role is created, modified, or assigned, publish an event. Cache consumers subscribe and evict affected entries:

typescript
// When a role changes — publish an invalidation event
await webhook.deliver({
  event: 'rbac.role-updated',
  scopeId: 'acme-corp',
  roleId: 'billing-admin',
  timestamp: new Date().toISOString(),
})

// Cache consumer — evict affected entries
webhook.subscribe('rbac.role-updated', async (event) => {
  // Evict all cached decisions for users in this scope who had this role
  await cache.deletePattern(`authz:*:${event.scopeId}:*:*`)
  // Conservative: evict everything for the scope
  // More precise: only evict users who held the changed role
})

The anti-pattern: never cache-and-forget

The one pattern to avoid: caching authorization decisions with long TTLs (minutes or hours) and no invalidation strategy. This is how "I revoked their access but they still could delete records for 45 minutes" happens. If your cache TTL is measured in minutes, you must have invalidation. If you don't have invalidation, your TTL must be measured in seconds.

WardenAuth's approach

WardenAuth runs authorization checks in 3-8ms — fast enough that most applications don't need cross-request caching. Request-scoped deduplication (via React cache() or equivalent) eliminates duplicate PDP calls within a request without any staleness risk. For high-throughput applications, webhook-driven invalidation keeps short-TTL caches correct.


Back to blogTry WardenAuth free →
© 2026 ecarrizo. All rights reserved.
PricingDocsCompareBlogLearnChangelogStatusGlossaryContactTermsPrivacy