JWT vs API Key Authentication: Complete Comparison Guide
Choosing between JWT (JSON Web Token) and API key authentication is one of the most consequential architectural decisions in API design. Both are valid approaches, but they solve different problems and carry different security trade-offs. This guide breaks down when to use each, how they work under the hood, and the hybrid patterns that combine the best of both.
How API Keys Work
An API key is an opaque string that the server generates and the client includes with every request. The server looks up the key in a database to identify the caller and determine their permissions.
# Client sends the API key in a header
curl -H "X-API-Key: sk_live_abc123def456" \
https://api.example.com/v1/users
# Server-side validation (pseudocode)
def authenticate(request):
api_key = request.headers.get('X-API-Key')
record = database.lookup(api_key) # Database lookup required
if not record or record.revoked:
return 401
return record.permissions
The key itself carries no information. It is simply a reference that the server resolves against its database. This means the server must perform a database lookup on every request to validate the key and retrieve the associated permissions.
How JWTs Work
A JWT is a signed token that contains claims (data) about the user or service. The token itself carries all the information needed for authorization, and the server validates it by verifying the cryptographic signature rather than querying a database.
# A JWT has three parts: header.payload.signature
# eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcxMDAwMH0.signature
# Decoded payload:
{
"sub": "user_123",
"role": "admin",
"permissions": ["read", "write", "delete"],
"exp": 1710000000, # Expires in 1 hour
"iss": "auth.example.com"
}
# Server-side validation (pseudocode)
def authenticate(request):
token = request.headers.get('Authorization').replace('Bearer ', '')
try:
claims = jwt.verify(token, public_key) # No database lookup
if claims.exp < now():
return 401
return claims.permissions
except InvalidSignature:
return 401
The critical difference: JWTs are self-contained. The server does not need to contact a database or authorization service to validate the token. It only needs the public key (for RSA/ECDSA) or shared secret (for HMAC) to verify the signature.
Side-by-Side Comparison
API Keys
- Opaque string, no embedded data
- Server-side validation (database required)
- No built-in expiration
- Instant revocation (delete from database)
- Simple to implement
- Best for server-to-server
- One lookup per request
- No standard format
JWTs
- Self-contained with claims/data
- Client-side validation (signature check)
- Built-in expiration (exp claim)
- Difficult to revoke before expiry
- More complex to implement correctly
- Best for user-facing APIs
- No database lookup needed
- Standardized (RFC 7519)
When to Use API Keys
Server-to-Server Communication
When two backend services need to communicate, API keys are often the simplest and most appropriate choice. The key identifies the calling service, and rate limits and permissions are enforced server-side. There is no user context to embed in a token, so the self-contained nature of JWTs provides no advantage.
Third-Party API Access
When you expose an API to external developers (like Stripe, Twilio, or SendGrid do), API keys are the industry standard. Developers generate a key in your dashboard, include it in their requests, and you track usage per key. The simplicity of this model is a significant advantage for developer experience.
Simple Rate Limiting and Billing
API keys map naturally to usage tracking and billing. Each key has an associated account, and you increment a counter on each request. With JWTs, you would need to extract the subject claim and perform a similar lookup, negating the "no database" advantage.
When Instant Revocation Is Critical
Because API keys are validated against a database, revoking a key takes effect immediately on the next request. This is critical for scenarios like employee departures, suspected compromise, or billing disputes.
When to Use JWTs
User Authentication in Web and Mobile Apps
JWTs shine when you need to authenticate users across multiple services. After a user logs in through your auth service, they receive a JWT that every microservice can validate independently without calling the auth service. This is the core use case that JWTs were designed for.
Microservice Architectures
In a system with 20+ microservices, having every service call a central auth database on every request creates a bottleneck. JWTs distribute the validation load: each service verifies the signature locally using the public key. No network call, no single point of failure.
# Each microservice validates the JWT independently
# Auth service issues the token:
token = jwt.encode({
"sub": "user_123",
"role": "admin",
"permissions": ["orders:read", "orders:write", "users:read"],
"exp": datetime.utcnow() + timedelta(hours=1)
}, private_key, algorithm="RS256")
# Order service validates without calling auth service:
claims = jwt.decode(token, public_key, algorithms=["RS256"])
if "orders:read" in claims["permissions"]:
return get_orders(claims["sub"])
Stateless APIs at Scale
When your API serves millions of requests per second, eliminating database lookups for authentication can meaningfully reduce latency and infrastructure costs. JWTs enable fully stateless request handling.
Cross-Domain and Single Sign-On (SSO)
JWTs can be passed across domains and services because they are self-contained. This makes them the foundation of most SSO implementations, including OpenID Connect (which is built on top of JWTs).
The Revocation Problem
The biggest security weakness of JWTs is revocation. Once a JWT is issued, it remains valid until it expires. If a user's account is compromised or their permissions change, the old JWT continues to work until the exp claim is reached.
Common solutions:
- Short expiration times. Set JWTs to expire in 15 minutes to 1 hour. Use refresh tokens (stored server-side) to issue new JWTs. This limits the window of vulnerability.
- Token blocklist. Maintain a list of revoked token IDs (the
jticlaim) and check it on each request. This adds a database lookup, partially negating the stateless advantage, but the blocklist is small compared to a full user database. - Token versioning. Store a "token version" per user. When revoking access, increment the version. JWTs include the version at issuance, and the server rejects tokens with old versions. Requires a lightweight cache lookup.
# Refresh token flow (most common pattern)
# 1. User logs in, receives both tokens
access_token = jwt.encode({"sub": "user_123", "exp": now + 15min}, key)
refresh_token = generate_opaque_token() # Stored in database
# 2. Client uses access_token for API calls (no DB lookup)
# 3. When access_token expires, client sends refresh_token
# 4. Server validates refresh_token (DB lookup), issues new access_token
# 5. To revoke: delete the refresh_token from database
# The access_token expires naturally within 15 minutes
Security Considerations
API Key Risks
- Leakage: API keys are long-lived and can be accidentally committed to repositories, logged, or exposed in client-side code. Once leaked, they work until manually revoked.
- No built-in scope: API keys do not inherently carry permission information. The server must look up permissions separately, which means misconfigured databases can grant excessive access.
- No identity context: An API key identifies a service or account, not a specific user. If you need to know which user performed an action through a service, the API key alone is insufficient.
JWT Risks
- Algorithm confusion attacks: If the server accepts multiple signing algorithms, an attacker can change the header to
"alg": "none"and bypass signature validation. Always validate the algorithm explicitly. - Secret key compromise: If the signing key is compromised, every JWT can be forged. Use RSA or ECDSA (asymmetric) instead of HMAC (symmetric) so that only the auth service holds the private key.
- Token size: JWTs are larger than API keys (often 500+ bytes) and are included in every request. This increases bandwidth usage and can hit header size limits in some configurations.
- Sensitive data exposure: JWT payloads are Base64-encoded, not encrypted. Anyone who intercepts a JWT can read its contents. Never include sensitive data (passwords, credit card numbers) in JWT claims.
The Hybrid Approach
Many production systems use both API keys and JWTs together:
- API keys for external developers. Third-party integrations use API keys for simplicity and instant revocation.
- JWTs for internal user authentication. User-facing web and mobile apps use JWTs with short expiration and refresh tokens.
- API keys that issue JWTs. An external service authenticates with an API key, then receives a short-lived JWT for subsequent requests. This combines the simplicity of API keys with the performance of JWTs.
# Hybrid: API key authenticates, JWT authorizes
# Step 1: Exchange API key for JWT
response = requests.post("https://auth.example.com/token", headers={
"X-API-Key": "sk_live_abc123"
})
jwt_token = response.json()["access_token"] # Valid for 1 hour
# Step 2: Use JWT for subsequent requests (no DB lookup per request)
data = requests.get("https://api.example.com/users", headers={
"Authorization": f"Bearer {jwt_token}"
})
There is no universal winner between JWTs and API keys. Use API keys for server-to-server communication, third-party integrations, and scenarios requiring instant revocation. Use JWTs for user authentication, microservice architectures, and high-scale stateless APIs. For most production systems, the best approach is a hybrid: API keys at the gateway, JWTs for internal authorization. Choose based on your specific requirements for revocation speed, scalability, and implementation complexity.
Recommended Resources
- Real-World Cryptography — understand the cryptographic foundations behind JWT signing algorithms (HMAC, RSA, ECDSA) discussed in this article.
- The Web Application Hacker's Handbook — learn how attackers exploit authentication weaknesses in both API key and JWT implementations.