The short answer
Use HS256 when a single application both issues and verifies its own tokens and the signing secret never has to leave that trust boundary. Use RS256 (or ES256) when more than one party needs to verify tokens — multiple services, third-party APIs, or any public client — because only the issuer ever needs to hold the private key, and verifiers use a freely distributable public key.
If you're unsure, RS256 is the safer default for anything that might grow beyond a single service. The cost is slightly larger tokens and a bit more key management; the benefit is that you never have to share a signing secret.
How HS256 works (symmetric)
HS256 is HMAC with SHA-256. It uses a single shared secretfor both signing and verifying. The issuer computes an HMAC over the encoded header and payload using the secret, and any party that wants to verify the token must hold that same secret and recompute the HMAC.
That symmetry is its strength and its weakness. It's simple and fast, with no key pairs to manage — perfect for a monolith that signs a token and checks it on the next request. But every service that needs to verify the token must possess the secret, and a secret shared with many services is a secret waiting to leak. Anyone who can verify a token can also forge one.
// HS256 — same secret signs and verifies const token = jwt.sign(claims, SECRET, { algorithm: "HS256" }); const result = jwt.verify(token, SECRET, { algorithms: ["HS256"] });
How RS256 works (asymmetric)
RS256 is RSA signature with SHA-256. It uses a key pair: a private key that only the issuer holds and signs with, and a public key that anyone can use to verify. The private key can produce signatures; the public key can only check them. Crucially, holding the public key does not let you forge tokens.
This solves the distribution problem cleanly. The issuer publishes its public key — typically at a JWKS endpoint like /.well-known/jwks.json— and any number of services, in any organisation, can verify tokens without ever touching the signing key. Identity providers such as Auth0, Okta, and "Sign in with Google" all use asymmetric signing for exactly this reason.
// RS256 — private key signs, public key verifies const token = jwt.sign(claims, PRIVATE_KEY, { algorithm: "RS256" }); const result = jwt.verify(token, PUBLIC_KEY, { algorithms: ["RS256"] });
Side-by-side
HS256 (symmetric)
- One shared secret for sign + verify.
- Fastest; smallest setup.
- Every verifier can also forge tokens.
- Best inside a single trust boundary.
- Secret distribution gets risky at scale.
RS256 (asymmetric)
- Private key signs, public key verifies.
- Public key is safe to distribute widely.
- Verifiers cannot forge tokens.
- Enables JWKS and key rotation.
- Larger signatures; key pair to manage.
When to choose each
Reach for HS256 when:
- A single service issues and verifies its own tokens (a classic monolith).
- The signing secret never has to leave that one service.
- You want the simplest possible setup and minimal token size.
Reach for RS256 (or ES256) when:
- Multiple services — possibly owned by different teams — must verify tokens.
- You expose tokens to third parties or public clients.
- You want to rotate keys without redistributing a secret to every verifier.
- You're implementing OpenID Connect or building an identity provider.
What about ES256 and EdDSA?
ES256 (ECDSA with the P-256 curve) and EdDSA(Ed25519) are also asymmetric, like RS256, but use elliptic-curve cryptography. They give you the same "private key signs, public key verifies" model with much smaller keys and signatures at equivalent security — an ES256 signature is a fraction of the size of an RS256 one, which keeps tokens compact.
For new systems that need asymmetric signing, ES256 or EdDSAare excellent modern defaults. RS256 remains the most widely supported and is the safe interoperability choice when you don't control every verifier. Whatever you pick, the golden rule from the best practices guide still applies: pin the algorithm on the server and never accept it from the token header.
Migrating from HS256 to RS256
If you started with HS256 and have outgrown it, you can migrate without a hard cutover:
- Generate an RSA key pair and publish the public key (ideally via a JWKS endpoint with a
kid). - Start signing new tokens with RS256, including the
kidin the header. - Configure verifiers to accept both algorithms during the transition, selecting the key by
kid— old HS256 tokens keep working until they expire. - Once all outstanding HS256 tokens have expired (one token TTL), drop HS256 from the accepted-algorithms list and retire the shared secret.
You can confirm which algorithm any token actually uses by decoding it and reading thealg field in the header — paste it into the decoder to check. For the errors that crop up during a migration, see how to fix common JWT errors.