iKit
Technical · 10 min read ·

HS256 vs RS256: Which JWT Algorithm Should You Pick (2026)

HS256 vs RS256 is the first decision when issuing a JWT — symmetric speed vs asymmetric key separation. Here's how to pick the right algorithm in 2026.

HS256 vs RS256: Which JWT Algorithm Should You Pick (2026)

HS256 vs RS256: Which JWT Algorithm Should You Pick

A JWT's first line decides everything else. {"alg":"HS256"} and {"alg":"RS256"} look almost identical, but one shares a single secret between issuer and verifier while the other splits the trust across a public and private key. Pick the wrong one and you're either rotating shared secrets across every microservice in your fleet or burning CPU on RSA math you didn't need. This guide walks through HS256 vs RS256 in 2026 — trade-offs, real numbers, and the decision tree.

TL;DR

  • HS256 is HMAC-SHA256 with one shared secret — the same key signs and verifies.
  • RS256 is RSA-SHA256 with a keypair — the private key signs, anyone with the public key verifies.
  • Pick HS256 only when issuer and verifier live inside the same trust boundary.
  • Pick RS256 the moment a third party needs to verify without holding the signing key.
  • Decode any token locally with the iKit JWT Decoder — pasting tokens into a server-side tool leaks them.

What HS256 and RS256 Actually Are

The JOSE family — RFC 7518 — defines every algorithm identifier a JWT header can use. The two that ship by default in every JWT library are HS256 and RS256. Their names are deliberately compact: H for HMAC, R for RSA, 256 for the SHA-256 hash they both ride on. The cryptographic primitive underneath is the only real difference, and that primitive dictates every key-management decision you'll make later.

HS256: HMAC over SHA-256

HS256 is HMAC-SHA256, defined by RFC 7518 §3.2 and built on RFC 2104. HMAC takes a shared secret and a message, runs SHA-256 over a salted construction, and emits a fixed 32-byte signature. There is exactly one key. The issuer uses it to sign; the verifier uses the same key to recompute the MAC and compare. If the key leaks, anyone with it can forge tokens that look authentic.

The minimum recommended secret length is the hash output size — 256 bits, or 32 bytes of cryptographically random data. Anything shorter is brute-forceable. iKit's password generator defaults to 32-byte random secrets for exactly this reason.

RS256: RSA with PKCS#1 v1.5 padding

RS256 is RSA-SHA256 with PKCS#1 v1.5 padding, defined by RFC 7518 §3.3. It uses an asymmetric keypair — a 2048-bit (or larger) RSA private key signs, and the matching public key verifies. The two keys are mathematically linked, but the public key by design reveals nothing useful about the private one. You can ship the public key in plain text inside a JWKS document; the private key never has to leave the issuer.

The signature itself is the size of the RSA modulus — 256 bytes for a 2048-bit key, 512 bytes for a 4096-bit key. That's roughly 8× larger than an HS256 signature, and it shows up on every request that carries the token.

What alg really is

The alg field in a JWT header is just a string lookup against the JOSE algorithm registry. The library on each end uses it to decide which primitive to run. That string is also one of the most-attacked surfaces in JWT history — see the section on alg: none below. The first thing a verifier should do, before anything else, is compare alg against an allowlist its operator picked at deploy time.

The Trust Boundary Test

Forget benchmarks for a moment. The single question that decides HS256 vs RS256 is who needs to verify a token?

One side signs, both sides see the key (HS256)

If your token is issued by service A and verified by service A — or by services A, B, and C that you fully own and can rotate together — HS256 is the right pick. Both sides need the secret, and you control both sides. The math is cheap, the signature is small, and key rotation is one config-map update.

One side signs, the world verifies (RS256)

If your token is issued by your auth service and verified by a mobile client, a third-party API, a partner backend, or a serverless edge worker you don't run, HS256 is wrong. Handing the verifier the signing key means the verifier can also issue tokens. RS256 separates the two roles: the issuer keeps the private key in a vault, and the verifier downloads the public half from a JWKS endpoint.

Why this is the only question that matters

Once you answer the trust-boundary question, the rest — performance, token size, library defaults — becomes implementation detail. Most teams that "agonize" over the choice are really worrying about whether they trust future-self not to leak the HS256 secret into a debug log or third-party tool.

Performance: What Each Actually Costs

The numbers below are typical for a modern x86-64 server running OpenSSL via Node 22 or Python 3.12. Hardware acceleration, ARM, and cold-start lambdas will differ.

Reading the numbers

Operation HS256 RS256 (2048) RS256 (4096)
Sign one token ~3 µs ~700 µs ~5,000 µs
Verify one token ~3 µs ~25 µs ~80 µs
Signature length 32 bytes 256 bytes 512 bytes

Two patterns stand out. Signing is where RSA hurts — the private-key operation is a couple of orders of magnitude slower than HMAC. Verification is much closer because the RSA public-key op uses a small public exponent (usually 65537) and finishes in tens of microseconds. The signature size shows up in every request: a typical JWT with RS256 is 600–900 bytes, with HS256 it's 250–400 bytes.

When the CPU bill matters

If you issue a million tokens a minute, the RSA signing cost is roughly 0.7 s per million tokens of single-core CPU. That's nothing for a dedicated auth service. It only becomes painful if you're considering signing fresh tokens on every request (you shouldn't — issue once, verify many).

When the bytes matter

Cookies max out at 4 KB. URL-embedded tokens are limited by browser URL length (~2 KB safely). If you're stuffing claims into a token and bumping the ceiling, switching from RS256 to ES256 saves ~200 bytes per token — but switching from RS256 to HS256 to save bytes is the wrong move, because the trust boundary changes.

Key Management Reality Check

This is where teams discover whether they made the right call.

Rotating an HS256 secret across N services

Every consumer of the token holds the secret. Rotation means: generate a new secret, push it to every service, give them a grace window where both old and new are accepted, then revoke the old one. With three services and a config-management pipeline you trust, it's a Tuesday afternoon. With twelve services across three cloud accounts and four teams, it's an outage waiting on a missed Slack message.

Rotating an RS256 keypair via JWKS

JWKS — JSON Web Key Set — solves rotation by exposing a list of currently-valid public keys at a well-known URL. Each key has a kid (key ID). The issuer signs new tokens with a new kid while still serving the old public key for the grace window. Verifiers cache JWKS for a few minutes and look the right key up by kid on every verify. Rotation becomes "add a new key, wait 24 h, remove the old one." Nothing else moves.

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "2026-05-current",
      "use": "sig",
      "alg": "RS256",
      "n": "0vx7agoebGcQS...",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "kid": "2026-02-retiring",
      "use": "sig",
      "alg": "RS256",
      "n": "uG5tWh6Fz4Lf...",
      "e": "AQAB"
    }
  ]
}

Why JWKS changed the calculus

Five years ago, "RS256 is more complex" was a real argument against it. JWKS plus the fact that every major library now caches and refreshes it automatically turned that complexity into a non-issue. The spec is dense; the code is one line — verify(token, jwksClient).

Real-World Defaults: Who Picks Which

A short survey of what providers sign with out of the box:

Provider Default alg Also supported
Auth0 RS256 HS256, PS256
Firebase Auth RS256
AWS Cognito RS256
Okta RS256 HS256
Keycloak RS256 HS256, ES256
Supabase HS256 RS256 (v2)

What the providers default to

Every major identity provider defaults to RS256 because the verifier is, by definition, someone else's service. Firebase, Cognito, and Auth0 publish a JWKS endpoint that every client SDK fetches automatically. Supabase historically defaulted to HS256 because the original architecture co-located the API and the auth service in one Postgres database — the trust boundary was zero. The newer Supabase Auth v2 moves toward RS256 precisely because third-party verifiers became a real use case.

Why libraries refuse to guess

Modern JWT libraries make you pass the expected algorithm explicitly to verify. They no longer trust the alg header on the token itself, because that's exactly the algorithm-confusion attack:

// VULNERABLE — trusts the token's header
jwt.verify(token, key);

// SAFE — pins the expected algorithm
jwt.verify(token, key, { algorithms: ['RS256'] });

Always pin the algorithm. If your verifier accepts both HS256 and RS256, an attacker can take the RSA public key you publish on your JWKS endpoint, sign a forged token using that public key as the HMAC secret with alg: HS256, and the verifier — if it trusts the header — happily validates it. The published alg value should always be the deployment's, never the token's.

When ES256 Quietly Beats Both

ES256 — ECDSA over the P-256 curve with SHA-256, RFC 7518 §3.4 — is the third algorithm in the JWT default trio. It produces a 64-byte signature (a quarter of RS256), signs in tens of microseconds, and offers the same asymmetric trust split as RS256. The trade-offs:

  • Smaller signatures. Matters when you're hitting URL or cookie limits.
  • Faster signing than RS256, roughly 10×.
  • Slightly slower verification than RS256 — RSA verify with a small public exponent is hard to beat.
  • Smaller keys. A 256-bit elliptic-curve key gives security comparable to a 3072-bit RSA key.
  • Less universal library support. Every JWT library handles HS256 and RS256; ES256 is well-supported in 2026 but you may still hit older systems that don't.

If you're greenfielding a new auth service in 2026, ES256 is a reasonable default. If you're integrating with a partner whose API spec was written in 2017, RS256 is what they expect.

Common Failure Modes

alg: none — the most-publicized JWT bug ever

A token with {"alg":"none"} claims to need no signature at all. Older libraries — notably node-jsonwebtoken before version 5 — would accept these as "signed but with no algorithm," meaning anyone could forge them. The fix is to never allow none and always pin the expected algorithm.

Trusting the alg header

Already covered above, but the attack pattern is short enough to fit on a slide:

const fake = jwt.sign(
  { sub: 'admin' },
  rsaPublicKeyPem,        // the SERVER's RSA public key
  { algorithm: 'HS256' }, // declared as HMAC
);
// If the server doesn't pin algorithms, this verifies.

The mitigation isn't tricky: pin the algorithm everywhere, and never let one verifier accept both symmetric and asymmetric variants.

Short HS256 secrets

A 16-byte (128-bit) secret is technically allowed but well within reach of an offline brute-force attack on a leaked token in commodity cloud time. Use 32 bytes (256 bits) minimum, generated with a cryptographic RNG, not a passphrase a human typed.

Reusing one HS256 secret across environments

The JWT_SECRET=devsecret line that survives into staging and then production is a leak waiting to happen. Use different secrets per environment, store them in a secrets manager, and rotate them on a schedule.

How to Pick in 2026 — A 60-Second Decision Tree

  1. Will any service outside your direct control verify these tokens?
    • Yes → RS256 (or ES256 if you control both ends and want smaller tokens).
    • No → continue.
  2. Do you have a secrets manager and a deploy pipeline that can rotate a shared secret across every consumer cleanly?
    • Yes → HS256 is fine.
    • No → RS256 anyway. JWKS is easier to operate than ad-hoc secret distribution.
  3. Are you integrating with an OIDC or OAuth identity provider (Auth0, Cognito, Firebase, Okta)?
    • Yes → you don't get to pick. It's RS256, verified via JWKS.

The lazy default in 2026 is RS256. The lazy default in 2018 was HS256. What changed is JWKS adoption — once verifiers can fetch public keys automatically, the operational cost of the asymmetric option fell below the cost of distributing shared secrets safely.

When you need to inspect a token to confirm which algorithm it actually uses, decode it locally with the iKit JWT Decoder — the alg field sits in the very first part of the token and decoding never touches the secret or the public key. If you're also generating cryptographic material for the HS256 case, the iKit hash generator covers SHA-256 verification and the password generator covers high-entropy secret generation, both in the browser, with nothing uploaded.

Related on iKit

Related posts