Skip to main content

jose vs jsonwebtoken vs fast-jwt: JWT Libraries for Node.js (2026)

·PkgPulse Team

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

PackageWeekly DownloadsEdge RuntimeJWKSJWE (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"
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

Featurejosejsonwebtokenfast-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.

Compare security and authentication packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.