JWT Signing Algorithms: HS256 vs RS256 vs ES256 Compared
Compare JWT signing algorithms HS256, RS256, and ES256. Understand symmetric vs asymmetric JWT signing, performance, and use cases.
In This Article
Choosing the Right JWT Signing Algorithm
The signing algorithm is one of the most critical security decisions in a JWT implementation. It determines how the token is signed, who can verify it, and how resistant it is to attacks. The three most commonly used algorithms are HS256, RS256, and ES256, each with distinct trade-offs.
The algorithm choice is specified in the JWT header's alg field. As explained in our What is JWT guide, the header is the first part of a JWT and tells the verifier how to validate the signature.
Symmetric vs Asymmetric Signing
Before comparing specific algorithms, it's essential to understand the fundamental difference:
Symmetric signing (HS256) uses a single shared secret for both signing and verification. Both the issuer and the verifier must possess the same secret key.
Asymmetric signing (RS256, ES256) uses a key pair: a private key for signing and a public key for verification. The private key stays secret on the issuer, while the public key can be distributed to any verifier.
This distinction has major architectural implications — especially in microservices and distributed systems where multiple services need to verify tokens without sharing secrets.
HS256 — HMAC with SHA-256
HS256 uses HMAC (Hash-based Message Authentication Code) with SHA-256. It's a symmetric algorithm that's fast and simple to implement.
How It Works
- Both parties share a secret key (at least 256 bits)
- The issuer creates a signature:
HMAC-SHA256(base64url(header) + "." + base64url(payload), secret) - The verifier recomputes the same HMAC and compares
Code Example
const jwt = require('jsonwebtoken');
const secret = process.env.JWT_SECRET; // at least 32 bytes
// Signing
const token = jwt.sign(
{ sub: 'user123', role: 'admin' },
secret,
{ algorithm: 'HS256', expiresIn: '1h' }
);
// Verification
const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] });
Pros and Cons
- Pros: Fast, simple, small token size, easy to implement
- Cons: Shared secret must be distributed to all verifiers, not suitable for multi-party verification, key rotation is complex
RS256 — RSA Signature with SHA-256
RS256 uses RSA (Rivest–Shamir–Adleman) with SHA-256 and PKCS#1 v1.5 padding. It's an asymmetric algorithm widely used in OAuth 2.0 and OpenID Connect.
How It Works
- The issuer generates an RSA key pair (minimum 2048-bit recommended)
- The issuer signs the token with the private key
- Any party with the public key can verify the signature
Code Example
const jwt = require('jsonwebtoken');
const fs = require('fs');
// Load RSA key pair
const privateKey = fs.readFileSync('private.pem');
const publicKey = fs.readFileSync('public.pem');
// Signing with private key
const token = jwt.sign(
{ sub: 'user123', role: 'admin' },
privateKey,
{ algorithm: 'RS256', expiresIn: '1h', keyid: 'key-1' }
);
// Verification with public key
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
Generating RSA Keys
# Generate a 2048-bit RSA private key
openssl genrsa -out private.pem 2048
# Extract the public key
openssl rsa -in private.pem -pubout -out public.pem
Pros and Cons
- Pros: Public key can be shared freely, ideal for multi-service architectures, widely supported, key rotation via JWKS
- Cons: Slower than HS256, larger token size (~256 bytes for signature), larger key sizes required (2048+ bits)
ES256 — ECDSA with P-256 and SHA-256
ES256 uses ECDSA (Elliptic Curve Digital Signature Algorithm) with the P-256 curve and SHA-256. It's a modern asymmetric algorithm recommended for new applications.
How It Works
- The issuer generates an EC key pair using the P-256 (secp256r1) curve
- The issuer signs the token with the private key
- Verification uses the public key — same concept as RSA but with smaller keys and faster operations
Code Example
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
// Generate EC key pair
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
namedCurve: 'P-256',
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
publicKeyEncoding: { type: 'spki', format: 'pem' },
});
// Signing
const token = jwt.sign(
{ sub: 'user123', role: 'admin' },
privateKey,
{ algorithm: 'ES256', expiresIn: '1h' }
);
// Verification
const decoded = jwt.verify(token, publicKey, { algorithms: ['ES256'] });
Pros and Cons
- Pros: Smaller keys (256-bit vs 2048-bit RSA), faster signing and verification, smaller signatures, modern and recommended by NIST cryptographic standards
- Cons: Less widely supported than RS256 in older libraries, signature is non-deterministic (two signatures of the same data differ)
Performance Comparison
| Metric | HS256 | RS256 | ES256 |
|--------|-------|-------|-------|
| Key size | 256 bits | 2048+ bits | 256 bits |
| Signature size | 32 bytes | 256 bytes | 64 bytes |
| Signing speed | ~100K ops/s | ~2K ops/s | ~10K ops/s |
| Verification speed | ~100K ops/s | ~20K ops/s | ~10K ops/s |
*Approximate numbers on modern hardware. Actual performance varies by library and implementation.*
HS256 is the fastest for both signing and verification. RS256 is slowest for signing but fast for verification (which matters more since verification happens on every request). ES256 offers a good balance with small keys and fast operations.
When to Use Each Algorithm
Use HS256 When:
- You have a single service that signs and verifies tokens
- The secret can be kept secure and shared safely
- You need maximum performance (e.g., high-throughput APIs)
- You're building a simple monolithic application
Use RS256 When:
- Multiple services need to verify tokens (microservices, API gateways)
- You're implementing OAuth 2.0 or OpenID Connect
- You need key rotation via RFC 7518 JWKS endpoints
- You want wide compatibility with existing identity providers
Use ES256 When:
- You're starting a new project and want modern cryptography
- Token size matters (mobile apps, constrained environments)
- You want faster signing than RSA with equivalent security
- Your identity provider supports it (Auth0, Okta, etc.)
Algorithm Confusion Attacks
One of the most dangerous JWT vulnerabilities is the algorithm confusion attack (also called algorithm switching). Here's how it works:
- A server expects RS256 tokens and has a public key
- An attacker changes the token header to
"alg": "HS256" - The attacker signs the forged token using the server's public key as the HMAC secret
- If the server blindly trusts the
algheader, it uses HS256 with the public key — which the attacker knows - The forged token passes verification
Prevention
Always hard-code the expected algorithm on the server side:
// SECURE: explicitly specify accepted algorithms
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
// INSECURE: trusting the algorithm from the token header
jwt.verify(token, publicKey); // vulnerable!
This is one of the key practices covered in our JWT security best practices guide. Never trust the algorithm specified in the token header — always enforce your expected algorithm server-side.
Key Management Best Practices
Key Storage
- Store private keys in a secure vault (AWS KMS, HashiCorp Vault, Google Cloud KMS)
- Never commit private keys to source control
- Use environment variables or secret management services
Key Rotation
- Use the
kid(Key ID) header parameter to identify which key signed the token - Publish public keys via a JWKS (JSON Web Key Set) endpoint
- Rotate keys periodically (at least annually) and immediately after a suspected compromise
Key Length
- HS256: minimum 256-bit (32-byte) secret
- RS256: minimum 2048-bit RSA key (4096 recommended)
- ES256: 256-bit EC key (fixed by the P-256 curve)
Conclusion
Choosing between HS256, RS256, and ES256 depends on your architecture, security requirements, and performance needs. HS256 is simplest for single-service apps, RS256 offers the widest compatibility for distributed systems, and ES256 provides the best efficiency for new projects. Regardless of your choice, always enforce the algorithm server-side and manage your keys securely. Use our free JWT decoder to inspect tokens and verify which algorithm is being used.
Need to decode a JWT token? Try our free JWT decoder tool — no sign-up required, runs entirely in your browser.