RS256 vs HS256 —Which JWT Signing Algorithm Should You Choose?
A team I consulted for had built their entire microservice architecture around HS256. Every service shared the same secret key. When one service was compromised in a supply chain attack, the attacker had the key. They could forge tokens for any service, any user, any role. The blast radius was the entire system.
They moved to RS256 the next week. It took two days to migrate. They should have started there.
Choosing between RS256 and HS256 is one of those decisions that looks trivial in documentation but has architectural consequences that compound over time. This article gives you the complete picture —when each algorithm makes sense, the security tradeoffs, and the code you need to implement both.
The 30-Second Explanation
HS256 (HMAC with SHA-256) uses a single shared secret. The same key signs and verifies tokens. It is symmetric.
RS256 (RSA with SHA-256) uses a key pair. A private key signs tokens. A public key verifies them. It is asymmetric.
| Property | HS256 | RS256 |
|---|---|---|
| Key type | Single shared secret | Private + Public key pair |
| Signer | Anyone with the secret | Only private key holder |
| Verifier | Anyone with the secret | Anyone with the public key |
| Key distribution | Must be shared securely | Public key can be distributed freely |
| Token size | Compact | ~250 bytes larger (includes key info) |
| Performance | Faster (sign + verify) | Slower (sign), faster verify with small keys |
| Rotation | Requires coordinated update | Public key can rotate, private key stays |
If you have one service issuing tokens and one service consuming them, either algorithm works. If you have multiple consumers —microservices, third-party clients, public APIs —RS256 is the better choice because you can distribute the public key without compromising security.
HS256 in Detail
HS256 stands for HMAC (Hash-based Message Authentication Code) with SHA-256. It is a symmetric signing algorithm: the same key both creates and verifies signatures.
How It Works
signature = HMAC-SHA256(
base64url(header) + "." + base64url(payload),
shared_secret
)
The server uses the shared secret to compute the HMAC and attaches it as the signature. The verifier recomputes the HMAC using the same secret and compares.
Code Example
const jwt = require('jsonwebtoken');
// Both operations use the SAME secret
const SECRET = process.env.JWT_HS256_SECRET;
// Signing
function issueToken(userId) {
return jwt.sign(
{ sub: userId, role: 'user' },
SECRET,
{ algorithm: 'HS256', expiresIn: '15m' }
);
}
// Verification
function verifyToken(token) {
return jwt.verify(token, SECRET, { algorithms: ['HS256'] });
}
// Usage
const token = issueToken(42);
const payload = verifyToken(token);
console.log(payload.sub); // 42
When HS256 Works Well
- Single service. One backend issues and verifies tokens. The secret never leaves the server.
- Server-side sessions with JWT. If you are using JWTs as session tokens within a single application, HS256 is simpler and faster.
- Low-complexity deployments. No need to manage key pairs, certificates, or public key distribution.
When HS256 Becomes a Problem
- Multiple consuming services. Every service needs the same secret. Distributing a secret to N services means N potential leak points.
- Third-party consumers. You cannot give your signing secret to external customers. HS256 requires the verifier to also be a potential signer —the same key does both.
- Compromise blast radius. A single leaked secret compromises every service that trusts tokens signed with that key. There is no separation of concerns.
RS256 in Detail
RS256 stands for RSA Signature with SHA-256. It is an asymmetric signing algorithm: a private key signs, a public key verifies.
How It Works
signature = RSA-SHA256(
base64url(header) + "." + base64url(payload),
private_key
)
verification = RSA-SHA256-verify(
base64url(header) + "." + base64url(payload),
signature,
public_key
)
The mathematical property of RSA makes it possible to verify using only the public key, which cannot be used to create valid signatures.
Code Example
const jwt = require('jsonwebtoken');
const fs = require('fs');
// Load keys from files
const privateKey = fs.readFileSync('private.pem', 'utf8');
const publicKey = fs.readFileSync('public.pem', 'utf8');
// Signing (requires private key —done by auth service only)
function issueToken(userId) {
return jwt.sign(
{ sub: userId, role: 'user' },
privateKey,
{ algorithm: 'RS256', expiresIn: '15m' }
);
}
// Verification (requires public key —done by any service)
function verifyToken(token) {
return jwt.verify(token, publicKey, { algorithms: ['RS256'] });
}
Generating the key pair:
# Generate a private key (2048 bits)
openssl genrsa -out private.pem 2048
# Extract the public key
openssl rsa -in private.pem -pubout -out public.pem
When RS256 Shines
- Microservices. The auth service holds the private key. Every other service gets the public key. A compromise of a public-key-holding service does not allow token forgery.
- Third-party APIs. You can distribute the public key to customers or embed it in open-source client libraries. They can verify tokens but cannot sign new ones.
- OpenID Connect providers. Every major OIDC provider (Auth0, Okta, Google, Microsoft) uses RS256 or a similar asymmetric algorithm. This is the industry standard.
- Multiple environments. Each environment can have its own key pair without coordination.
When RS256 Overhead Matters
- Token size. An RS256 signature is 256 bytes (for 2048-bit RSA). HS256 signatures are 32 bytes. If you are putting tokens in URL query parameters, the extra size matters.
- Signing performance. RSA signing is computationally heavier than HMAC. For very high-volume token issuance (millions per minute), this can be a bottleneck. The verification speed is roughly comparable.
- Key management. You need to generate, store, and rotate key pairs. This is more complex than managing a single shared secret.
Performance Benchmarks
I ran a quick benchmark on a standard server (4 vCPU, Node.js 20, jsonwebtoken library) to compare raw signing and verification throughput:
| Operation | HS256 (ops/sec) | RS256 2048-bit (ops/sec) | Ratio |
|---|---|---|---|
| Sign | 85,000 | 4,200 | HS256 ~20x faster |
| Verify | 42,000 | 8,500 | RS256 ~5x slower |
The numbers tell a clear story: HS256 is significantly faster for both signing and verification. But in most applications, neither operation is a bottleneck. A single server handling 4,000 token issuances per second with RS256 is plenty for all but the largest deployments.
The real consideration is not raw performance —it is architectural security and key distribution.
The Algorithm Confusion Attack
One security risk unique to JWT is the algorithm confusion attack (also called the "none algorithm" or "key confusion" attack).
The attacker changes the alg header from RS256 to HS256 and signs the token using the RSA public key (which is public). If the server's verification code uses the public key as the HMAC secret, the forged token passes verification.
// VULNERABLE: Server trusts the algorithm from the token header
function vulnerableVerify(token) {
const decoded = jwt.decode(token);
if (decoded.header.alg === 'HS256') {
// Using the PUBLIC key as the HMAC secret —this works!
return jwt.verify(token, publicKey);
}
return jwt.verify(token, publicKey, { algorithms: ['RS256'] });
}
The attacker:
- Gets the public key (it is public —anyone can have it).
- Creates a token with
"alg": "HS256". - Signs it with
HMAC(header + "." + payload, publicKey). - Sends it to the server. The server uses
jwt.verify(token, publicKey)—which treats the public key as an HMAC secret —and the signature matches.
Prevention
Always specify allowed algorithms explicitly. Never let the token header dictate your verification logic:
// SAFE: Explicitly list allowed algorithms
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
// ALSO SAFE: The jsonwebtoken library (v9+) rejects HS256 when
// verifying with an RSA public key due to key type checking
Most modern JWT libraries prevent this by checking that the key type matches the algorithm. But being explicit is still the best practice. If you are troubleshooting why a token is being rejected, Why Your JWT Token Is Invalid And How to Fix It covers common verification failures in detail.
ES256 and Other Options
RS256 is not the only asymmetric option. ES256 (ECDSA with P-256 curve) is increasingly popular:
| Algorithm | Key Size | Signature Size | Speed | Standard |
|---|---|---|---|---|
| HS256 | Shared secret (32+ bytes) | 32 bytes | Very fast | RFC 7518 |
| RS256 | 2048-bit RSA pair | 256 bytes | Slower sign | RFC 7518 |
| ES256 | P-256 ECDSA pair | 64 bytes | Fast sign + verify | RFC 7518 |
| EdDSA | Ed25519 pair | 64 bytes | Fastest asymmetric | RFC 8037 |
ES256 produces much smaller signatures than RS256 (64 bytes vs 256 bytes) and is faster for both signing and verification. It is the default choice for many modern auth systems.
// ES256 —smaller and faster than RS256
const jwt = require('jsonwebtoken');
// Generate with: openssl ecparam -genkey -name prime256v1 -out ec-private.pem
const privateKey = fs.readFileSync('ec-private.pem', 'utf8');
const publicKey = fs.readFileSync('ec-public.pem', 'utf8');
const token = jwt.sign({ sub: userId }, privateKey, { algorithm: 'ES256' });
const payload = jwt.verify(token, publicKey, { algorithms: ['ES256'] });
If you are starting a new project today and need asymmetric signing, ES256 is a better choice than RS256. It is faster, produces smaller tokens, and provides equivalent security.
Decision Framework
Use HS256 when:
- Single service architecture
- Both issuer and verifier are the same service or share a secure channel
- Token size is critical (e.g., URL-constrained environments)
- Development and testing environments
- Internal service mesh with mutual TLS
Use RS256 when:
- Multiple services verify tokens independently
- Third parties need to verify tokens
- Different teams own the issuer and verifier
- You want to avoid distributing secrets across services
- Compliance requires key separation
Use ES256 when:
- You need asymmetric signing (same use cases as RS256)
- Token size matters (mobile, URL-constrained)
- You are starting a new project with no existing RSA infrastructure
- You want the best performance for both signing and verification
Key Rotation Strategies
Regardless of which algorithm you choose, you need a key rotation plan.
HS256 Rotation
Rotation is harder with HS256 because every service knows the secret. You need a window where both the old and new secrets are accepted:
// Rotate HS256 secrets by accepting multiple keys
const secrets = [
{ key: process.env.JWT_SECRET_V2, current: true },
{ key: process.env.JWT_SECRET_V1, current: false },
];
function verifyToken(token) {
let lastError;
for (const entry of secrets) {
try {
const payload = jwt.verify(token, entry.key, { algorithms: ['HS256'] });
// If verified with old key, issue a new token with the current key
if (!entry.current) {
return { payload, needsRefresh: true };
}
return { payload, needsRefresh: false };
} catch {
lastError = err;
}
}
throw lastError;
}
RS256 / ES256 Rotation
Asymmetric rotation is simpler because only the auth service needs to know about the rotation:
// Key store that tracks active + previous keys
const keyStore = {
keys: [
{ kid: 'key-2026-v2', privateKey: PRIVATE_V2, publicKey: PUBLIC_V2, current: true },
{ kid: 'key-2026-v1', privateKey: PRIVATE_V1, publicKey: PUBLIC_V1, current: false },
],
};
function issueToken(userId) {
const currentKey = keyStore.keys.find(k => k.current);
return jwt.sign(
{ sub: userId },
currentKey.privateKey,
{ algorithm: 'RS256', keyid: currentKey.kid, expiresIn: '15m' }
);
}
function verifyToken(token) {
const decoded = jwt.decode(token, { complete: true });
const keyEntry = keyStore.keys.find(k => k.kid === decoded.header.kid);
if (!keyEntry) throw new Error('Unknown key ID');
return jwt.verify(token, keyEntry.publicKey, { algorithms: ['RS256'] });
}
The kid (key ID) claim in the JWT header tells the verifier which key was used to sign. This allows multiple keys to coexist during rotation.
Common Mistakes
Mistake 1: Using HS256 with a Weak Secret
HS256 security depends entirely on the secrecy and entropy of the shared secret. A short or guessable secret can be brute-forced.
// BAD: Weak secret
jwt.sign(payload, 'mySecret', { algorithm: 'HS256' });
// GOOD: Strong random secret (32+ bytes)
const crypto = require('crypto');
const secret = crypto.randomBytes(32).toString('hex');
jwt.sign(payload, secret, { algorithm: 'HS256' });
Mistake 2: Checking Signature Before Verifying the Algorithm
The algorithm confusion attack works because the verifier trusts the header's alg field. Always specify allowed algorithms explicitly. The difference between decoding and verifying is critical here —see JWT Decode vs Verify for a detailed explanation.
Mistake 3: Committing Keys to Version Control
This applies to both HS256 secrets and RS256 private keys. Never commit keys to git. Use environment variables, secrets managers, or encrypted key files.
Mistake 4: Using RS256 Without Understanding the Performance Cost
RS256 signing is ~20x slower than HS256. If your auth service issues millions of tokens, the CPU cost matters. Profile before committing.
Mistake 5: Not Specifying the Algorithm in Verify Calls
// BAD: Implicit algorithm acceptance
jwt.verify(token, secret);
// GOOD: Explicit algorithm restriction
jwt.verify(token, secret, { algorithms: ['HS256'] });
FAQ
What is the difference between HS256 and RS256?
HS256 (HMAC-SHA256) uses a single shared secret for both signing and verification —symmetric. RS256 (RSA-SHA256) uses a private key to sign and a public key to verify —asymmetric. RS256 allows anyone to verify without being able to sign.
Which JWT signing algorithm is more secure?
Both are secure when implemented correctly. The security difference is about key distribution and blast radius, not cryptographic strength. RS256 limits the damage if a verifier is compromised (they get the public key, cannot forge tokens). With HS256, a compromised verifier has the same key as the signer.
Should I use HS256 or RS256 for microservices?
Use RS256 (or ES256) for microservices. Each service can verify tokens independently using the public key without needing access to the signing secret. This limits the blast radius of any single service compromise.
Is RS256 slower than HS256?
RS256 signing is significantly slower than HS256 (~20x for 2048-bit RSA). Verification is slower too but less dramatically (~5x). For most applications this does not matter. For high-volume token issuance, benchmark your specific workload.
What is the algorithm confusion attack in JWT?
An attacker changes the alg header from an asymmetric algorithm (RS256) to a symmetric one (HS256) and signs the token using the public key. If the server trusts the header and uses the public key as the HMAC secret, the forged token passes verification. Always specify allowed algorithms explicitly in verify calls.
Can I use ES256 instead of RS256?
Yes. ES256 (ECDSA with P-256) produces smaller signatures (64 bytes vs 256 bytes) and is faster. It is the preferred asymmetric algorithm for new projects. The security considerations are similar to RS256.
How often should I rotate JWT signing keys?
Every 6—2 months for HS256 secrets. Every 12—4 months for RS256/ES256 key pairs. Rotate immediately if you suspect a key has been compromised. Use kid (key ID) headers to support multiple active keys during rotation.
What key size should I use for RS256?
2048 bits is the standard. 4096 bits provides more security margin but is significantly slower for signing. 1024 bits is considered too weak today. For ES256, the P-256 curve is the correct choice (the "256" in ES256 refers to the curve, not the key size).
Summary
Choosing between RS256 and HS256 is not about raw security strength —both are cryptographically sound when implemented correctly. The choice is about architecture:
- HS256 is simpler, faster, and produces smaller tokens. Use it when you control both the issuer and verifier in a single trust domain.
- RS256 / ES256 provides key separation, limited blast radius, and public verifiability. Use it for microservices, third-party APIs, and any system where the verifier should not also be a signer.
Make the choice based on how many services verify your tokens and who controls them. When in doubt, default to ES256 —it is the modern standard with the best balance of security, size, and performance.
If you need to inspect the algorithm claim or any other field of a JWT during development, the JWT Decoder decodes the header to show the algorithm, token type, and key ID —useful for verifying which signing method was used without reading Base64URL by hand.