You've picked JWT for auth. Now you're staring at algorithm options: HS256 or RS256. Both work. Both are widely supported. But they're solving slightly different problems, and picking the wrong one for your architecture will bite you later.
Here's the practical breakdown.
What's Actually Different
Both algorithms produce a signed JWT. The difference is how signing and verification work.
HS256 (HMAC-SHA256) — symmetric:
- One secret key signs the token
- The same secret key verifies it
- Anyone who can verify can also forge tokens
RS256 (RSA-SHA256) — asymmetric:
- A private key signs the token (only the auth server holds this)
- A public key verifies it (safe to distribute freely)
- Verifiers cannot forge new tokens
This distinction matters the moment you have more than one service touching your tokens.
The Shared Secret Problem with HS256
With HS256, every service that verifies JWTs needs the secret key. In a microservices setup, that means:
- The secret is stored in 5, 10, 20 places
- A breach in any downstream service exposes the signing key
- Any of those services could issue valid tokens
That last point is the real risk. A compromised order service shouldn't be able to mint auth tokens — but with HS256, it technically can.
RS256 eliminates this. Your auth server keeps the private key locked down. Every other service gets the public key (often via a JWKS endpoint). They can verify but never issue.
Performance Tradeoff
HS256 is faster — HMAC is a simple hash operation. RSA signing is computationally heavier.
In practice: this rarely matters. JWT signing happens once per login. Verification is more frequent but still fast. Unless you're signing thousands of tokens per second on constrained hardware, the difference is negligible.
Implementing Both
Generating RSA Key Pairs
You'll need actual PEM files for RS256. Generate them with OpenSSL:
# Generate 2048-bit private key
openssl genrsa -out private.pem 2048
# Extract the public key
openssl rsa -in private.pem -pubout -out public.pemFor production, use at least 2048-bit keys. 4096-bit adds more security with a modest performance cost — reasonable for auth servers that aren't signing millions of tokens per second.
Key Rotation
RS256 has a clear operational advantage here.
With HS256, rotating your secret means every service with a copy needs updating simultaneously — a coordination nightmare.
With RS256, you:
- Generate a new key pair on the auth server
- Publish the new public key to your JWKS endpoint
- Old tokens signed with the old key still verify (keep old public key in JWKS temporarily)
- Remove old keys after token TTLs expire
Services automatically pick up new keys from JWKS without any coordination. This is why most OAuth 2.0 and OIDC providers (Google, Auth0, Okta) use RS256 and expose a /.well-known/jwks.json endpoint.
What About ES256?
Worth a quick mention: ES256 (ECDSA with P-256) is a modern alternative to RS256. It's also asymmetric but uses smaller keys with equivalent security. Signatures are shorter, and it's faster than RSA.
If you're starting fresh and your libraries support it, ES256 is worth considering. But RS256 has broader library support and is the safer default for interoperability.
Common Mistakes to Avoid
- Accepting
alg: none— Always explicitly specify which algorithms your verifier accepts. Never allownone. - Using the same secret across environments — Dev, staging, and prod should have separate secrets/keys.
- Putting sensitive data in JWT payloads — The payload is base64-encoded, not encrypted. Anyone with the token can read it. Encrypt the payload or keep it minimal.
- Long-lived tokens without refresh — Short expiry + refresh tokens is better than long-lived JWTs regardless of algorithm.
Use HS256 when: you have a single service that both issues and verifies tokens, your setup is simple, and you can tightly control where the secret lives.
Use RS256 when: multiple services verify tokens, you need a public JWKS endpoint, you're building for third-party integrations, or you want cleaner key rotation.
For anything beyond a toy project, RS256 is the right default. The operational benefits outweigh the marginal complexity of managing a key pair. Your future self — debugging a secret-sprawl incident at 2am — will thank you.