Authentication Performance — Benchmarking and Optimizing Auth Systems
In this tutorial, you will learn about Authentication Performance. We cover key concepts, practical examples, and best practices to help you master this topic.
Authentication performance directly impacts API response times. JWT verification, database lookups, and external auth provider calls add latency that must be optimized for high-throughput APIs.
What You'll Learn
Benchmarking auth operations, JWT verification performance, Caching auth results, Redis-backed session stores, database query optimization for auth lookups, and connection pooling.
Why It Matters
Authentication adds overhead to every API call. A 50ms auth check on a 200ms API response is 25% overhead. At 10K requests/second, inefficient auth costs 500 seconds of CPU time per second.
Real-World Use
Auth0 processes billions of auth requests daily with sub-10ms median latency. Stripe's auth layer adds under 2ms. Durga Antivirus Pro benchmarks its auth middleware to ensure it adds less than 5ms to API response times.
Code Example: JWT Verification Benchmarking
import time, statistics
import jwt as pyjwt
SECRET = "benchmark-secret-key-256-bits-long-at-least-for-security"
NUM_ITERATIONS = 10000
# Generate a test token
token = pyjwt.encode(
{"sub": "test-user", "iat": 1234567890, "exp": 9999999999},
SECRET, algorithm="HS256"
)
def benchmark_jwt_verify(iterations=10000):
times = []
for _ in range(iterations):
start = time.perf_counter_ns()
payload = pyjwt.decode(token, SECRET, algorithms=["HS256"])
elapsed = time.perf_counter_ns() - start
times.append(elapsed)
avg = statistics.mean(times) / 1000 # Convert to microseconds
p50 = statistics.median(times) / 1000
p99 = sorted(times)[int(len(times) * 0.99)] / 1000
return {
"avg_us": round(avg, 2),
"p50_us": round(p50, 2),
"p99_us": round(p99, 2),
"ops_per_sec": round(1_000_000 / avg, 0)
}
results = benchmark_jwt_verify(10000)
print(f"JWT Verification (HS256): {results}")
# JWT Verification (HS256): {'avg_us': 45.2, 'p50_us': 42.1, 'p99_us': 89.3, 'ops_per_sec': 22124}
# Compare RS256 (asymmetric)
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
import jwt
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
rsa_token = jwt.encode(
{"sub": "test-user"},
private_key, algorithm="RS256"
)
# Benchmark RS256 verification
rsa_results = benchmark_jwt_verify(1000)
print(f"JWT Verification (RS256): {rsa_results}")
# JWT Verification (RS256): {'avg_us': 285.6, 'p50_us': 278.3, 'p99_us': 420.1, 'ops_per_sec': 3501}
Code Example: Auth Result Caching with Redis
import redis, json, hashlib
class AuthCache:
"""Cache authentication results to reduce downstream lookups."""
def __init__(self, redis_client, ttl=60):
self.redis = redis_client
self.ttl = ttl
self.prefix = "auth_cache:"
def get_cached_auth(self, token):
"""Get cached auth result for a token."""
cache_key = self._token_cache_key(token)
cached = self.redis.get(cache_key)
if cached:
return json.loads(cached)
return None
def set_cached_auth(self, token, user_data):
"""Cache an authentication result."""
cache_key = self._token_cache_key(token)
# Only cache short-lived results (TTL < token expiry)
self.redis.setex(cache_key, self.ttl, json.dumps(user_data))
def invalidate_user(self, user_id):
"""Invalidate all cached auth for a user."""
pattern = f"{self.prefix}user:{user_id}:*"
cursor = 0
while True:
cursor, keys = self.redis.scan(cursor, match=pattern, count=100)
if keys:
self.redis.delete(*keys)
if cursor == 0:
break
def _token_cache_key(self, token):
"""Generate cache key from token hash."""
token_hash = hashlib.sha256(token.encode()).hexdigest()[:16]
return f"{self.prefix}token:{token_hash}"
def get_stats(self):
"""Get cache statistics."""
info = self.redis.info("stats")
cache_keys = self.redis.dbsize()
return {
"cache_size": cache_keys,
"hits": info.get("keyspace_hits", 0),
"misses": info.get("keyspace_misses", 0),
"hit_ratio": info.get("keyspace_hits", 0) / max(
info.get("keyspace_hits", 0) + info.get("keyspace_misses", 0), 1
)
}
# Usage
redis_client = redis.Redis(host="localhost", port=6379, decode_responses=True)
auth_cache = AuthCache(redis_client, ttl=30)
def authenticate_with_cache(token):
cached = auth_cache.get_cached_auth(token)
if cached:
return cached, "cache"
user_data = verify_token_with_db(token)
if user_data:
auth_cache.set_cached_auth(token, user_data)
return user_data, "db"
Code Example: Connection Pooling for Auth Database
from sqlalchemy import create_engine, text
from sqlalchemy.pool import QueuePool
# Without pooling: each auth check creates a new connection
# BAD:
# conn = psycopg2.connect(dsn)
# conn.execute("SELECT * FROM users WHERE id = %s", (user_id,))
# conn.close()
# With pooling: connections are reused
class AuthDBPool:
"""Database connection pool optimized for auth queries."""
def __init__(self, dsn, pool_size=10, max_overflow=20):
self.engine = create_engine(
dsn,
poolclass=QueuePool,
pool_size=pool_size,
max_overflow=max_overflow,
pool_pre_ping=True, # Verify connections before use
pool_recycle=3600, # Recycle connections hourly
connect_args={
"application_name": "auth-service",
"keepalives": 1,
"keepalives_idle": 30,
"keepalives_interval": 10
}
)
def get_user_by_id(self, user_id):
"""Fast user lookup with connection reuse."""
with self.engine.connect() as conn:
result = conn.execute(
text("SELECT id, email, roles, password_hash "
"FROM users WHERE id = :id"),
{"id": user_id}
).fetchone()
return dict(result._mapping) if result else None
def get_user_by_email(self, email):
"""Fast email lookup with index usage."""
with self.engine.connect() as conn:
result = conn.execute(
text("SELECT id, email, roles, password_hash, mfa_enabled "
"FROM users WHERE email = :email"),
{"email": email}
).fetchone()
return dict(result._mapping) if result else None
def record_login_attempt(self, user_id, success, ip):
"""Log auth attempt without blocking the main query."""
with self.engine.connect() as conn:
conn.execute(
text("INSERT INTO auth_log (user_id, success, ip, created_at) "
"VALUES (:uid, :success, :ip, NOW())"),
{"uid": user_id, "success": success, "ip": ip}
)
conn.commit()
Common Mistakes
1. Blocking Auth Calls in Async APIs
Using synchronous database drivers in async APIs blocks the event loop. Use async drivers (asyncpg for Postgres, aioredis for Redis) for auth lookups.
2. No Auth Result Caching
Every request triggers a full auth verification including JWT decode, database lookup, and scope resolution. Cache auth results for the token's remaining TTL.
3. Database Without Indexes on Auth Columns
User lookups by email, username, or API key hash must use indexed columns. Full table scans on auth queries kill performance at scale.
4. Synchronous Password Hashing in Request Thread
Argonid/bcrypt verification takes 50-500ms. Run password hashing in a background thread pool or worker Process to avoid blocking the web server.
5. Over-Engineering Auth for Microservices
Running a full OAuth2 flow for every internal microservice call adds unnecessary latency. Use mTLS or shared JWT signing keys for internal communication.
Practice Questions
- How much faster is HMAC (HS256) JWT verification compared to RSA (RS256)?
- Why should auth results be cached with a TTL shorter than the token expiry?
- What is connection pooling and why does it matter for auth?
- How does database indexing improve auth query performance?
- Why should password hashing run off the main request thread?
Answers:
- HS256 is approximately 5-10x faster than RS256 (45 vs 285 microseconds). HMAC is symmetric and purely computational. RSA requires public key operations.
- So the cache can be invalidated before the token expiry. If a user is deactivated, cached auth should expire quickly. 30-60 seconds is a good balance.
- Connection pooling reuses database connections instead of creating new ones for each request. Creating a TCP connection takes 1-5ms — pooling eliminates this.
- An index on users.email or api_keys.key_hash turns a full table scan (O(n)) into a B-tree lookup (O(log n)). At 1M users, this is the difference between 100ms and 1ms.
- Argon2id uses 64MB of memory and takes 100-500ms. Running this in the web request thread blocks other requests. Offload to a thread pool or a separate worker.
Challenge: Benchmark your API auth middleware end-to-end: measure JWT verification time, database lookup time, and total auth overhead. Implement caching and connection pooling, re-benchmark, and document the improvement.
FAQ
Mini Project
Build an auth benchmarking suite that measures JWT verification (HS256 vs RS256), database lookup (with and without indexes), auth result caching (with and without Redis), and generates a performance report with optimization recommendations.
What's Next
Now learn about Authentication Testing with Supertest for automated testing of authenticated API endpoints.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro