HS256 vs RS256: Which JWT Signature to Choose

November 9, 2026

Setting up authentication reaches the line where the library asks you to pick a token signing algorithm, and the dropdown offers HS256, RS256, ES256. The choice looks like a small detail while it decides big things: where the keys live, who will be able to verify your tokens, and how a later move from a monolith to microservices will go. This article covers the two main algorithms in practice: what stands behind each name, which one fits common situations, where the line between enough and already dangerous runs, and how the algorithm substitution attack works, the one that pushed libraries into demanding an explicit alg pinning.

Two families: a shared secret and a key pair

The names decode literally. HS256 is HMAC-SHA-256: the signature is a hash function mixed with a secret key, and the same secret both creates and verifies the signature. RS256 is RSA-SHA-256: the signature is created with a private key and verified with the paired public key. Every practical difference between the two algorithms follows from a single distinction: symmetry versus asymmetry.

  • HS256: only the holder of the secret can issue and verify a token; once the secret leaks, everyone who obtained it can forge signatures, and a forgery is indistinguishable from a genuine one;
  • RS256: only the holder of the private key, usually the authorization server, signs; anyone with the public key can verify, and the public key needs no protection;
  • the infrastructure consequence: a monolith with one codebase is fine with HS256, while an architecture where many independent services verify tokens asks for RS256;
  • the performance consequence: HMAC is two passes of a hash function, RSA is modular arithmetic over large numbers, and HS256 is orders of magnitude cheaper to compute.

HS256: one secret for everyone

Verification in HS256 comes down to recomputation. The server takes the first two parts of the token, computes HMAC-SHA-256 with its copy of the secret, and compares the result with the third part. A match means the token was issued by whoever knows the secret and the content traveled unchanged. How the HMAC mathematics work, why signatures must not be compared with plain equality, and how long the secret should be, all of that is covered in the article about HMAC request signing.

The practical consequence of symmetry: the secret must live wherever tokens get verified. Every verifying service becomes a full owner of the secret, with the same rights as the issuer, including the right to issue tokens. Three verifying services mean three storage locations for one secret and three leak surfaces. Rotating a compromised secret lands on everyone at once: the issuer signs new tokens with the new secret while a verifier still configured with the old one rejects perfectly valid tokens until it gets updated.

HMAC-SHA-256: the basic operation of HS256
const crypto = require('crypto');
crypto.createHmac('sha256', 'secret').update('hello').digest('hex');

// the result:
88aab3ede8d3adf94d26ab90d3bafd4a2083070c3bcce9c014ee04a443847c0b

The vector above reproduces in the HMAC generator: message hello, key secret, algorithm SHA-256. This is exactly the operation a server performs on every HS256 token, except that instead of the word hello it signs the string made of the first two token parts joined by a dot.

RS256: private signs, public verifies

The authorization server keeps the private key and signs tokens with it. Every verifier uses the public key: it can go into a public repository, be handed to a partner, or be published at an address like /.well-known/jwks.json, the standard pattern for authentication providers. Knowing the public key does not enable signing a single token: signing requires the private one, and that is the entire point of the scheme.

  • a microservice verifies tokens without a shared secret: the public key is enough, and compromising the microservice grants no right to issue tokens;
  • external consumers: a partner gateway or an outside developer can verify your tokens without receiving any secret from you;
  • rotation: the private key changes on the issuer while verifiers pick up the new public key from JWKS automatically, and the kid field in the token header tells them which key signed it;
  • incident containment: when the private key leaks, the problem stays inside the authorization server, and verifiers only refresh the key.

Compared point by point

Here are the differences as a list: the value and what it means in practice.

  • signature size: HS256 fits in 43 base64url characters (32 bytes), RS256 with a 2048-bit key takes 342 base64url characters (256 bytes); a shorter token means less overhead on every request and every log entry;
  • speed: generating an RSA signature costs orders of magnitude more than HMAC-SHA-256, RSA verification is cheaper than generation yet still pricier than HMAC; the gap shows on workloads verifying many tokens per second;
  • who can verify: HS256 only holders of the secret, RS256 anyone holding the public key;
  • key distribution: the HS256 secret travels to every verifier over a protected channel, the RS256 public key is published openly;
  • compromise: a leaked HS256 secret equals a leaked right to sign, a leaked RS256 public key gives an attacker nothing;
  • key rotation: HS256 requires updating the secret on every verifier, RS256 updates a single private key on the issuer.

When HS256 is enough, and when RS256 is needed

  • a monolith or an application where issuing and verifying the token live in one codebase under one team;
  • two internal services under your control whose configuration ships from a single repository;
  • background jobs and queues: your own worker signs and your own scheduler verifies;
  • a prototype and an early product: while there is one verifier, asymmetry solves a problem that does not exist.

The criterion is simple: if the same party issues and verifies the token, HS256 wins on simplicity. No key pairs, no JWKS, no private key storage procedure: one secret of at least 32 random bytes in the configuration store, and the topic is closed.

The picture flips as soon as there are several verifiers that stop depending on each other:

  • several services verify tokens, especially in different languages and repositories: handing a shared secret to five teams multiplies the leak surfaces, while a public key is published once;
  • client applications or external developers should be able to verify tokens themselves: the public key exists exactly for this;
  • authentication is delegated to a separate provider: ready-made solutions such as Keycloak, Auth0 or AWS Cognito issue RS256 tokens by default, and consumers fetch the keys from JWKS at the address given in their configuration;
  • planned key rotation without downtime: kid and JWKS cover the period when old and new signatures coexist;
  • a containment requirement: a web server holding only a public key cannot issue a single token, no matter how many times it gets compromised.

Verifying a token in practice

Start with reading. How a token is built, what the three parts carry and why the payload must never hold secrets, is covered in the article about the anatomy of a JWT. The JWT decoder unfolds the header and the payload instantly, but it displays the signature without verifying it: verification needs the key and happens on the side the token is addressed to. Here is a token signed with HS256 under the key secret:

An HS256 token and its verification
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzQyIiwicm9sZSI6InZpZXdlciIsImV4cCI6MTgwMDAwMDAwMH0.Ci3X_62brDlB-LGPnZwEDiQx0gidMXeY3uMgefb3P8A

// header: {"alg":"HS256","typ":"JWT"}
// payload: {"sub":"user_42","role":"viewer","exp":1800000000}
// exp = 1800000000 is January 15, 2027, 08:00 UTC

const crypto = require('crypto');
const [h, p] = token.split('.');
crypto.createHmac('sha256', 'secret').update(h + '.' + p).digest('base64url');
// 'Ci3X_62brDlB-LGPnZwEDiQx0gidMXeY3uMgefb3P8A' — matches the third part

The signature is computed over the string of the first two parts with a dot between them, and the vector is reproducible: with the same key secret and the same message the HMAC generator produces the same result in hex form, 0a2dd7ffad9bac3941f8b18f9d9c040e2431d2089d317798dee32079f6f73fc0. Change one word in the payload, the role viewer to editor, and the payload encoding shifts, so the old signature stops matching. An attacker cannot compute the new signature without the secret, and the entire protection rests on that.

Building and signing your own tokens is what the JWT generator does: it signs HS256, and the rule stays the same as with manual computation: identical header and payload strings with an identical secret always yield an identical signature. In code, the check looks the same for both algorithms; only the key argument differs:

Library verification: HS256 and RS256
const jwt = require('jsonwebtoken');

// HS256: the second argument is the secret string
jwt.verify(token, 'secret', { algorithms: ['HS256'] });

// RS256: the second argument is the public key in PEM format
jwt.verify(token, publicKeyPem, { algorithms: ['RS256'] });

// pinning the algorithm through the algorithms option is not paranoia:
// it is the defense against the attack described below

The alg confusion attack: why alg must not be trusted

Vulnerabilities of the alg confusion family exploit one detail: the verifying code reads alg from the token header and adapts to the arriving value. Write-ups from 2015 demonstrated two classic variants of the attack.

  1. alg: none. The attacker strips the third part of the token, edits the payload and sets alg to none in the header: no signature needed. Libraries that trusted the header accepted such a token as if it were signed;
  2. RS256 disguised as HS256. The attacker takes the server public key, which is public by definition, computes an HMAC over it as if it were the secret, and sets alg to HS256. The verifier, obediently switching to HMAC, compares the signature against the public key in the role of the secret and accepts the forgery: everything the attacker needed to craft it was already in their hands.

The fix takes one line: the verifier does not read alg from the token but pins the expected value in configuration, algorithms: ['RS256'], and rejects everything else before even touching the signature. Modern libraries either demand an explicit algorithm list or bind every key to exactly one algorithm. HS256 and RS256 themselves are innocent: both are sound algorithms, and the hole appears the moment the choice of cryptography is delegated to attacker-controlled data.

Common mistakes

  • the HS256 secret in client code: a frontend bundle or a mobile app carries the secret inside the built artifact, and client-side verification becomes handing the signing key to every user;
  • the RS256 private key in a browser or an app: a private key by definition lives only on the authorization server, everything else is infrastructure for forging tokens;
  • an HS256 secret shorter than 32 bytes or assembled from dictionary words: a short phrase is brute-forced from a dictionary because checking one guess costs microseconds;
  • trusting the alg header without pinning the algorithm list: an open door to the alg confusion from the previous section;
  • one secret for every environment: a staging environment sharing the production secret turns a staging leak into a production compromise;
  • comparing signatures with hand-written string equality instead of the library function: the timing difference of the comparison is measurable, details in the article about HMAC.

Frequently asked questions

Can you migrate from HS256 to RS256 without downtime?

Yes. The issuer starts producing RS256 tokens while verifiers pin a transitional list, algorithms: ['HS256', 'RS256'], and hold both keys. Old HS256 tokens live out their exp, usually an hour at most, after which the list narrows to one algorithm. What matters is that both values on the transition are pinned explicitly rather than taken from the token header.

What about HS384, HS512, RS384, RS512 and ES256?

The same families with different hash functions: HS512 is HMAC-SHA-512 with a 64-byte signature, RS512 is RSA with SHA-512; the bare SHA-512 hash without a key is computed by the SHA-512 generator, but a bare hash is not a signature, the key in HMAC is mandatory. ES256 belongs to the third family, elliptic curves: comparable strength with a much shorter signature, around 64 bytes. The selection logic repeats HS256 versus RS256, with size and speed as secondary differences.

How much slower is RS256?

Generating an RSA signature costs orders of magnitude more than HMAC-SHA-256; RSA verification is cheaper than generation yet still clearly pricier than HMAC. Exact numbers depend on key length, library and hardware, which is why no universal figure is quoted. For a typical web application verifying one token per request the difference is invisible; it shows up on services verifying tens of thousands of tokens per second, where teams deploy RS256 with a public key cache and measure with their own benchmark.

Where should the RS256 private key be stored?

In a secret store: a key management service, environment variables with restricted access, or a dedicated configuration store. Not in a repository, not in a Docker image, not in client code. Access to the private key equals the right to issue tokens as any user, so the circle of people with access stays minimal and operations on the key are logged.

Are 32 characters enough for the HS256 secret?

The minimum is 32 random bytes, not 32 characters of meaningful text. A string of dictionary words is weaker than 16 truly random bytes, because the guessing runs over a dictionary rather than over all byte combinations. The rule: take 32 to 64 bytes from a cryptographic generator and put them into the secret store; a length without entropy guarantees nothing.

The choice boils down to one question: who verifies your tokens. One party, one service: HS256, 32 random bytes in the secret store, topic closed. Several independent verifiers: RS256, the private key only at the issuer, the public one distributed through JWKS. Moving between the schemes is possible without downtime, so the choice is a starting point, not a sentence.

Related articles