Multi-region auth: session management across geographic deployments

Running your application in multiple regions improves latency and provides geographic redundancy. But auth adds complexity to multi-region deployments that does not exist for stateless services. Sessions are state. JWKS keys are state. Revocation lists are state. The question is not whether to replicate auth state across regions but how to do it without creating race conditions, stale revocation, or data residency violations.

JWT verification without a central auth server

The main advantage of JWTs is that verification is local — any service with the public key can verify a token without calling the auth server. In multi-region deployments, this means token verification latency does not depend on inter-region round trips. The complication is key distribution: each region needs a current copy of the JWKS, and key rotation must propagate correctly.

// JWKS caching with background refresh
// jose library handles this internally when using createRemoteJWKSet,
// but understanding the TTL strategy matters for multi-region

import { createRemoteJWKSet, jwtVerify } from 'jose';

// Per-region JWKS cache — each region points to the auth server's JWKS
// The JWKS endpoint should have a long cache-control max-age during steady state
// and a short max-age during key rotation
const JWKS = createRemoteJWKSet(
  new URL('https://auth.yourapp.com/.well-known/jwks.json'),
  {
    cacheMaxAge: 10 * 60 * 1000,   // 10 minutes — balance freshness vs load
    cooldownDuration: 30 * 1000,    // 30 seconds between forced refreshes
  }
);

async function verifyToken(token) {
  try {
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: 'https://auth.yourapp.com',
      algorithms: ['ES256'],
    });
    return payload;
  } catch (err) {
    // jose will retry JWKS fetch on kid mismatch (catches rotation)
    throw err;
  }
}

Key rotation in multi-region deployments

Key rotation requires care to avoid verification failures during the transition. The correct procedure: publish the new key in JWKS before using it to sign any new tokens. Leave the old key in JWKS for at least the maximum token lifetime after you stop signing with it. This ensures tokens signed with the old key remain verifiable until they expire.

# JWKS endpoint response during rotation
# Both old and new keys present; new key has higher priority but either verifies
{
  "keys": [
    {
      "kty": "EC",
      "crv": "P-256",
      "kid": "ec-key-2022-06",   // OLD key — still in JWKS for verification
      "use": "sig",
      "x": "...",
      "y": "..."
    },
    {
      "kty": "EC",
      "crv": "P-256",
      "kid": "ec-key-2022-07",   // NEW key — used for all new tokens
      "use": "sig",
      "x": "...",
      "y": "..."
    }
  ]
}
Regional JWKS caches will pick up the new key at their next cache refresh interval. During this window, tokens signed with the new key may fail verification at regions whose cache has not refreshed yet. To minimize this, pre-warm regional caches by hitting the JWKS endpoint from each region immediately after publishing the new key, before switching signing to the new key.

Session storage: sticky vs replicated

If your auth system uses server-side sessions (rather than stateless JWTs), you need a cross-region session strategy. The two main approaches are sticky sessions and session replication.

Sticky sessions pin each user to a specific region via a cookie or DNS routing. Once authenticated in us-east-1, all subsequent requests go to us-east-1. This is simple but fragile — a regional outage logs out all users pinned to that region and creates uneven load distribution based on where users signed up.

Session replication stores sessions in a globally replicated store. Redis Enterprise with active-active geo-replication, or CockroachDB for relational session data, can replicate across regions with sub-second lag. Users can be served by any region. The trade-off is write latency and conflict resolution for concurrent session updates.

// Session store backed by Redis with regional fallback
import Redis from 'ioredis';

const PRIMARY_REGION = process.env.AWS_REGION;  // e.g., 'us-east-1'

// Connect to local regional replica first, primary as fallback
const sessionStore = new Redis.Cluster([
  { host: `redis.${PRIMARY_REGION}.internal`, port: 6379 },
  { host: 'redis.us-west-2.internal', port: 6379 },
  { host: 'redis.eu-west-1.internal', port: 6379 },
], {
  scaleReads: 'slave',        // read from nearest replica
  redisOptions: {
    tls: {},
    connectTimeout: 500,      // fail fast, don't block requests on cache misses
  }
});

// Session TTL: align with JWT expiry to avoid orphaned sessions
const SESSION_TTL = 3600;  // 1 hour

async function getSession(sessionId) {
  const data = await sessionStore.get(`session:${sessionId}`);
  return data ? JSON.parse(data) : null;
}

async function setSession(sessionId, data, ttl = SESSION_TTL) {
  await sessionStore.setex(`session:${sessionId}`, ttl, JSON.stringify(data));
}

Data residency constraints

GDPR and data sovereignty laws in some jurisdictions (Germany, Brazil, Russia, China) require that personal data — including auth session data containing user identifiers — be stored and processed within specific geographic boundaries. This creates a direct conflict with global session replication: you cannot replicate EU user sessions to US servers and comply with GDPR's data transfer restrictions simultaneously.

The practical solution is tenant-aware regional routing. Each organization or user is assigned a home region at signup based on their country or explicit selection. Auth sessions for that user are stored in the home region only. Cross-region API requests carry the JWT (stateless, no PII beyond the user ID and claims) and the JWT is verified locally. The session — which may contain richer profile data — is only accessible in the home region.

// Tenant-region mapping: each org has a designated data region
// SELECT region FROM organizations WHERE id = $1
// Returns: 'us-east-1', 'eu-west-1', 'ap-southeast-1', etc.

// In the auth middleware: route session lookups to the correct region
async function authenticateRequest(req) {
  const token = extractBearerToken(req);
  const payload = await verifyToken(token);  // local, no cross-region call

  // Enrich with session data only if needed
  // Route enrichment call to the tenant's home region
  if (needsSessionData(req)) {
    const regionEndpoint = getRegionalEndpoint(payload.org_region);
    const session = await fetchFromRegion(regionEndpoint, payload.session_id);
    req.session = session;
  }

  req.user = payload;
}

Token verification latency benchmarks

Local JWT verification (ES256, cached JWKS key) takes roughly 0.1–0.3ms in Node.js. A cross-region JWKS fetch adds 50–200ms depending on the region pair. The implication: aggressive JWKS caching (10–30 minute TTL) is correct for steady state. During key rotation, accept the brief period where some regional caches are stale and let the jose library's kid-mismatch retry handle recovery. Do not set JWKS TTL below 5 minutes in production — the load on the auth server's JWKS endpoint from hundreds of services refreshing frequently becomes significant at scale.

← Back to blog Try Bastionary free →