BlogHow to Rotate JWT Secrets Without Downtime
·Updated July 22, 2026·5 min read·JWTSecrets Team

How to Rotate JWT Secrets Without Downtime

Rotate JWT signing secrets in production without logging out users using the grace-period pattern and kid-based key selection.

Why Naive JWT Secret Rotation Breaks Production

Rotating a JWT secret sounds straightforward until you realise the moment you change it, every active user session signed with the old secret gets a 401. Done naively, a secret rotation is a forced logout for your entire user base — support tickets, abandoned carts, and broken mobile sessions included.

The fix is the grace-period pattern: temporarily accept tokens signed by both the old and the new secret while you wait for short-lived access tokens to expire naturally. This guide shows how to implement that pattern correctly, including production-grade kid selection for multi-service systems.

Generate replacement material with the JWT Secret Generator before you start — never invent secrets by hand.

Why You Need to Rotate JWT Secrets

Several scenarios require secret rotation:

Suspected compromise. If your secret was ever committed to a repository, logged, or visible in an error trace, treat it as compromised. Rotate immediately.

Team member departure. If a developer who had access to your secrets leaves the organisation, rotate all secrets they could access.

Compliance requirements. SOC 2, ISO 27001, and PCI-DSS all require periodic cryptographic key rotation — typically annually at minimum.

Security hygiene. Even without a specific trigger, rotating secrets annually limits the window of exposure from any undetected compromise.

Rotation is not optional for long-lived HMAC deployments. Treat signing secrets like passwords for your entire auth surface: unique per environment, stored outside git, and scheduled for change.

The Grace-Period Pattern

Maintain two valid secrets simultaneously during the rotation window. Sign all new tokens with the new secret. Continue to accept tokens signed by either secret until all old tokens expire naturally.

// secrets.js
const jwt = require('jsonwebtoken');

const secrets = {
  current: process.env.JWT_SECRET_CURRENT,  // new secret
  previous: process.env.JWT_SECRET_PREVIOUS // old secret (keep during grace period)
};

function signToken(payload) {
  return jwt.sign(payload, secrets.current, {
    algorithm: 'HS256',
    expiresIn: '15m'
  });
}

function verifyToken(token) {
  try {
    return jwt.verify(token, secrets.current, { algorithms: ['HS256'] });
  } catch (e) {
    if (secrets.previous) {
      return jwt.verify(token, secrets.previous, { algorithms: ['HS256'] });
    }
    throw e;
  }
}

module.exports = { signToken, verifyToken };

Pin algorithms: ['HS256'] on every verify call. Omitting the allowlist invites algorithm confusion attacks while you are mid-rotation.

Step-by-Step Rotation Process

Step 1 — Generate the new secret.

Use a CSPRNG to generate a fresh 256-bit secret. Do not reuse any previous secret. Create yours with the JWT Secret Generator.

Step 2 — Deploy with both secrets.

Set the new secret as JWT_SECRET_CURRENT and the old one as JWT_SECRET_PREVIOUS. Deploy. Your verify function now accepts both.

Step 3 — Wait for old tokens to expire.

With 15-minute access tokens, all old tokens are invalid within 15 minutes of the dual-secret deploy. With longer-lived tokens (1 hour, 24 hours), wait that full duration plus clock-skew buffer.

Step 4 — Remove the old secret.

After all old tokens have expired, remove JWT_SECRET_PREVIOUS from your environment. Redeploy. Rotation complete — zero users were logged out by the secret swap itself.

Document the runbook. Practice it in staging with deliberately short TTLs so you can validate the overlap window in minutes, not hours.

Using the `kid` Header for Production-Grade Rotation

For systems that need explicit key identification (microservices, JWKS endpoints), use the kid (key ID) header:

const jwt = require('jsonwebtoken');

const KEY_STORE = {
  v3: process.env.JWT_SECRET_V3, // current
  v2: process.env.JWT_SECRET_V2, // previous (during grace period)
};

function signToken(payload) {
  return jwt.sign(payload, KEY_STORE.v3, {
    algorithm: 'HS256',
    expiresIn: '15m',
    keyid: 'v3',
  });
}

function verifyToken(token) {
  const decoded = jwt.decode(token, { complete: true });
  const kid = decoded?.header?.kid;
  const secret = KEY_STORE[kid];

  if (!secret) throw new Error('Unknown key ID');
  return jwt.verify(token, secret, { algorithms: ['HS256'] });
}

The kid header makes key selection explicit — no trial-and-error, and you know exactly which key signed each token. Prefer this pattern when multiple services verify tokens independently. For algorithm trade-offs, see HS256 vs RS256.

Common Rotation Pitfalls

  • Swapping secrets without an overlap window (instant mass logout).
  • Leaving the previous secret mounted forever (defeats the point of rotation).
  • Rotating access-token secrets but forgetting long-lived refresh-token binding.
  • Logging tokens or secrets during the transition.
  • Using different secret names per service without a shared rotation calendar.

Production Checklist

  • [ ] New secret generated with a CSPRNG (not derived from a password)
  • [ ] Dual-secret or multi-kid verify path deployed first
  • [ ] New issuances use only the current secret / kid
  • [ ] Grace period ≥ max access-token TTL + skew
  • [ ] Previous secret removed after the window
  • [ ] Staging rehearsal completed
  • [ ] Incident notes updated if rotation followed a suspected leak

Frequently Asked Questions

How do I rotate secrets in Kubernetes?

Update your Kubernetes Secret with the new value and use kubectl rollout restart to trigger a rolling deployment. For the grace-period pattern, use two separate Secret keys and mount both as environment variables so pods accept previous and current during the overlap.

Do refresh tokens need to be invalidated during rotation?

Refresh tokens are often validated against a database record, not only a signature. Rotation of the access-token HMAC secret does not automatically invalidate refresh sessions — expire them in the database or include a key/version field that refresh validation checks when compromise is suspected.

What is the minimum grace period for 15-minute access tokens?

Fifteen minutes is the mathematical minimum after the dual-secret deploy. In practice, use about 30 minutes to account for clock skew, slow rollouts, and clients that refresh slightly late.

Should I create a new URL slug when rewriting a rotation guide?

No. Keep stable slugs such as /blog/how-to-rotate-jwt-secrets so existing internal links and search rankings continue to resolve. Expand the body in place when the title already matches the intent.

Written by

JWTSecrets Team

Editorial Team

The JWTSecrets editorial team writes practical guides on JWT authentication, cryptographic key management, and browser-based security tooling. Our content is reviewed against IETF RFCs and current library documentation.