jose vs jsonwebtoken vs fast-jwt: JWT Libraries for Node.js (2026)
TL;DR
jose is the modern standard for JWT in 2026 — it uses the Web Crypto API, works in every runtime (Node.js, Deno, Bun, Cloudflare Workers, browsers), supports JWK/JWKS for rotating keys, and handles JWE (encrypted tokens) in addition to JWS (signed tokens). jsonwebtoken is the legacy choice — simple, synchronous API, but Node.js-only, no edge runtime support, and slower for RS256. fast-jwt is the performance-focused alternative with worker thread support and async flows. For any new project in 2026, use jose.
Key Takeaways
- jose: ~5M weekly downloads — Web Crypto API, all runtimes, JWK/JWKS, JWE/JWS/JWK
- jsonwebtoken: ~12M weekly downloads — legacy, sync API, Node.js-only, widely used
- fast-jwt: ~700K weekly downloads — fastest Node.js JWT, worker threads, sync + async
- jose works in Cloudflare Workers, Vercel Edge, Deno, Bun — jsonwebtoken does not
- JWKS (JSON Web Key Set) for rotating public keys is a jose-native feature
- For RS256 (asymmetric) tokens, jose is significantly faster than jsonwebtoken
Download Trends
| Package | Weekly Downloads | Edge Runtime | JWKS | JWE (Encryption) |
|---|---|---|---|---|
jose | ~5M | ✅ All runtimes | ✅ | ✅ |
jsonwebtoken | ~12M | ❌ Node.js only | ❌ | ❌ |
fast-jwt | ~700K | ❌ Node.js only | ❌ | ❌ |
jose
jose is the comprehensive JOSE (JSON Object Signing and Encryption) library — covers JWT, JWS, JWE, JWK, JWKS, and more.
Signing and verifying JWTs (HS256 — symmetric)
import { SignJWT, jwtVerify } from "jose"
const secret = new TextEncoder().encode(process.env.JWT_SECRET) // HMAC-SHA256 key
// Sign:
async function signToken(payload: { userId: string; role: string }) {
return new SignJWT(payload)
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("1h")
.setIssuer("pkgpulse.com")
.setAudience("pkgpulse-api")
.sign(secret)
}
const token = await signToken({ userId: "user-123", role: "admin" })
// Verify:
async function verifyToken(token: string) {
const { payload } = await jwtVerify(token, secret, {
issuer: "pkgpulse.com",
audience: "pkgpulse-api",
})
return payload as { userId: string; role: string }
}
const claims = await verifyToken(token)
console.log(claims.userId) // "user-123"
console.log(claims.role) // "admin"
RS256 (asymmetric — recommended for distributed systems)
import { SignJWT, jwtVerify, generateKeyPair, exportJWK, importJWK } from "jose"
// Generate key pair (do this once, store keys securely):
const { privateKey, publicKey } = await generateKeyPair("RS256")
// Export keys as JWK for storage:
const privateJwk = await exportJWK(privateKey)
const publicJwk = await exportJWK(publicKey)
// Sign with private key:
async function signRSA(payload: object) {
const privKey = await importJWK(privateJwk, "RS256")
return new SignJWT(payload as any)
.setProtectedHeader({ alg: "RS256", kid: "my-key-id" })
.setIssuedAt()
.setExpirationTime("15m") // Short-lived — use refresh tokens
.sign(privKey)
}
// Verify with public key only:
async function verifyRSA(token: string) {
const pubKey = await importJWK(publicJwk, "RS256")
const { payload } = await jwtVerify(token, pubKey)
return payload
}
JWKS — rotating keys (OAuth2/OIDC pattern)
import { createRemoteJWKSet, jwtVerify } from "jose"
// Fetch public keys from JWKS endpoint (Auth0, Clerk, Google, etc.):
const JWKS = createRemoteJWKSet(
new URL("https://your-tenant.us.auth0.com/.well-known/jwks.json")
)
// Verify JWT from any identity provider:
async function verifyAuth0Token(token: string) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: "https://your-tenant.us.auth0.com/",
audience: "https://api.pkgpulse.com",
})
return payload
}
// Clerk example:
const ClerkJWKS = createRemoteJWKSet(
new URL("https://api.clerk.com/v1/jwks")
)
async function verifyClerkToken(sessionToken: string) {
const { payload } = await jwtVerify(sessionToken, ClerkJWKS)
return payload as {
sub: string // User ID
email: string
metadata: Record<string, unknown>
}
}
Edge runtime (Cloudflare Workers, Vercel Edge)
// app/api/auth/middleware.ts — Vercel Edge Runtime
export const runtime = "edge"
import { jwtVerify } from "jose"
const secret = new TextEncoder().encode(process.env.JWT_SECRET)
export async function middleware(request: Request) {
const authHeader = request.headers.get("authorization")
if (!authHeader?.startsWith("Bearer ")) {
return new Response("Unauthorized", { status: 401 })
}
const token = authHeader.slice(7)
try {
const { payload } = await jwtVerify(token, secret)
// Add user info to request headers for downstream handlers:
const headers = new Headers(request.headers)
headers.set("x-user-id", String(payload.sub))
headers.set("x-user-role", String(payload.role))
return fetch(request, { headers })
} catch {
return new Response("Invalid token", { status: 401 })
}
}
jsonwebtoken
jsonwebtoken — the legacy standard, still widely used in Node.js-only apps:
Basic sign/verify
import jwt from "jsonwebtoken"
const SECRET = process.env.JWT_SECRET!
// Synchronous sign (simpler but blocks):
const token = jwt.sign(
{ userId: "user-123", role: "admin" },
SECRET,
{
expiresIn: "1h",
issuer: "pkgpulse.com",
algorithm: "HS256",
}
)
// Synchronous verify:
try {
const payload = jwt.verify(token, SECRET, {
issuer: "pkgpulse.com",
}) as { userId: string; role: string }
console.log(payload.userId)
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
console.error("Token expired")
} else if (err instanceof jwt.JsonWebTokenError) {
console.error("Invalid token")
}
}
// Async with callback:
jwt.sign({ userId: "123" }, SECRET, { expiresIn: "1h" }, (err, token) => {
if (err) throw err
console.log(token)
})
RS256 with files
import jwt from "jsonwebtoken"
import { readFileSync } from "fs"
const privateKey = readFileSync("./keys/private.pem")
const publicKey = readFileSync("./keys/public.pem")
// Sign with private key:
const token = jwt.sign({ userId: "user-123" }, privateKey, {
algorithm: "RS256",
expiresIn: "15m",
})
// Verify with public key:
const payload = jwt.verify(token, publicKey, {
algorithms: ["RS256"],
})
fast-jwt
fast-jwt — fastest Node.js JWT with caching and worker thread support:
import { createSigner, createVerifier } from "fast-jwt"
// Create reusable signer/verifier (compiles once, reuses):
const sign = createSigner({ key: process.env.JWT_SECRET! })
const verify = createVerifier({ key: process.env.JWT_SECRET! })
// Sign (async by default):
const token = await sign({ userId: "user-123", role: "admin" })
// Verify:
const payload = await verify(token)
console.log(payload.userId)
// Synchronous (faster if you're sure about the blocking):
const signSync = createSigner({ key: process.env.JWT_SECRET!, sync: true })
const verifySync = createVerifier({ key: process.env.JWT_SECRET!, sync: true })
const tokenSync = signSync({ userId: "123" })
const payloadSync = verifySync(tokenSync)
// Caching — fastest verification for repeated tokens:
const cachedVerify = createVerifier({
key: process.env.JWT_SECRET!,
cache: true, // Cache decoded tokens
cacheTTL: 60000, // Cache for 60 seconds
})
Feature Comparison
| Feature | jose | jsonwebtoken | fast-jwt |
|---|---|---|---|
| Edge runtimes | ✅ All | ❌ Node.js only | ❌ Node.js only |
| JWKS support | ✅ Built-in | ❌ | ❌ |
| JWE (encryption) | ✅ | ❌ | ❌ |
| ES256/RS256/PS256 | ✅ All | ✅ | ✅ |
| Synchronous API | ❌ Async only | ✅ | ✅ |
| Token caching | ❌ | ❌ | ✅ |
| Worker threads | ❌ | ❌ | ✅ |
| TypeScript | ✅ | ✅ @types | ✅ |
| Performance (RS256) | ✅ | ⚠️ Slow | ✅ Fastest |
| Maintenance | ✅ Active | ✅ Active | ✅ Active |
When to Use Each
Choose jose if:
- You need edge runtime support (Cloudflare Workers, Vercel Edge)
- You need JWKS for verifying tokens from Auth0, Clerk, Google, or other OIDC providers
- You need JWE (encrypted tokens) for sensitive payload data
- You're starting a new project — most modern and feature-complete
Choose jsonwebtoken if:
- Legacy Node.js app already using it — no reason to migrate what works
- You want the synchronous
jwt.sign()/jwt.verify()API - Simple HS256 tokens with no JWKS needs
Choose fast-jwt if:
- Performance is critical — high-traffic API verifying thousands of tokens/second
- You want token caching to avoid repeated cryptographic operations
- Node.js only app where edge runtime isn't needed
Methodology
Download data from npm registry (weekly average, February 2026). Performance benchmarks from fast-jwt repository. Feature comparison based on jose v5.x, jsonwebtoken v9.x, and fast-jwt v3.x.