Skip to content

Authentication Performance — Benchmarking and Optimizing Auth Systems

DodaTech Updated 2026-06-28 6 min read

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

  1. How much faster is HMAC (HS256) JWT verification compared to RSA (RS256)?
  2. Why should auth results be cached with a TTL shorter than the token expiry?
  3. What is connection pooling and why does it matter for auth?
  4. How does database indexing improve auth query performance?
  5. Why should password hashing run off the main request thread?

Answers:

  1. HS256 is approximately 5-10x faster than RS256 (45 vs 285 microseconds). HMAC is symmetric and purely computational. RSA requires public key operations.
  2. 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.
  3. Connection pooling reuses database connections instead of creating new ones for each request. Creating a TCP connection takes 1-5ms — pooling eliminates this.
  4. 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.
  5. 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

What is the target auth overhead per request?

Aim for under 5ms total auth overhead for a typical API request. JWT verification should be under 100 microseconds, database lookups under 2ms with proper indexing.

Should I use HS256 or RS256 for JWT?

Use HS256 for internal services (faster, shared secret). Use RS256 for external APIs or when the verifier and issuer are different services (public key distribution).

Does Redis speed up auth operations?

Redis lookup takes under 1ms. Caching verified auth results in Redis avoids repeated database queries and JWT decoding for the same token.

How do I profile auth performance?

Use application performance monitoring (APM) tools like Datadog APM or New Relic. Add custom spans for auth operations: token decode, DB lookup, scope resolution.

What is the performance impact of MFA?

MFA adds a separate verification step (TOTP check or SMS code). TOTP is fast (under 1ms). SMS code verification involves storage lookup only.

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