Skip to content

JWT validation + renewal

Verifies JWT signatures with HMAC SHA-256, checks expiry, and optionally reissues tokens — all using built-in Web Crypto, no external JWT package. This example shows what it looks like to offload auth-related crypto work to a worker pool.

The host builds a batch of JWTs (mix of valid and expired). Each job verifies the signature, checks the renewal window (renewWindowSec, renewUntil), and either returns the validated claims or issues a fresh token. The result comes back as a stringified JSON response — keeping structured-clone overhead low.

Three files:

  • jwt_knitting.ts — runs revalidation in host and Knitting modes
  • utils.ts — token verification, renewal logic, task exports
  • bench_jwt_revalidation.ts — host-vs-worker benchmark with mitata

Input to the task is a JSON string like this:

{
"token": "<jwt>",
"nowSec": 1767225600,
"ttlSec": 180,
"renewWindowSec": 30
}

The task returns a JSON string. A successful non-renewed response looks like:

{
"ok": true,
"renewed": false,
"token": "<same-jwt>",
"sub": "user_42",
"sid": "session_42",
"exp": 1767225750,
"canRenew": true
}

If the token is near expiry but still renewable, the same shape comes back with renewed: true and a replacement token.

deno.sh
deno add --npm jsr:@vixeny/knitting
deno add npm:mitata

Uses built-in Web Crypto APIs — no extra JWT package required.

bun.sh
bun src/jwt_knitting.ts

Expected output:

-- host mode --
verified: 900 renewed: 80 rejected: 20
-- knitting mode (2 threads) --
verified: 900 renewed: 80 rejected: 20
bun.sh
bun src/bench_jwt_revalidation.ts

Compares verify + optional renewal + JSON.stringify via direct imports (host) against the same workload through worker task calls (knitting). Batched dispatch keeps the comparison stable.

jwt_knitting.ts
import { createPool, isMain } from "@vixeny/knitting";
import {
buildDemoRevalidateRequests,
type RenewalSummary,
revalidateToken,
revalidateTokenHost,
summarizeJsonResponses,
} from "./utils.ts";
const THREADS = 2;
const REQUESTS = 25_000;
const INVALID_PERCENT = 10;
async function runHost(rawRequests: string[]): Promise<RenewalSummary> {
const outputs = new Array<string>(rawRequests.length);
for (let i = 0; i < rawRequests.length; i++) {
outputs[i] = await revalidateTokenHost(rawRequests[i]!);
}
return summarizeJsonResponses(outputs);
}
async function runWorkers(rawRequests: string[]): Promise<RenewalSummary> {
const pool = createPool({ threads: THREADS })({ revalidateToken });
try {
const jobs: Promise<string>[] = [];
for (let i = 0; i < rawRequests.length; i++) {
jobs.push(pool.call.revalidateToken(rawRequests[i]!));
}
const outputs = await Promise.all(jobs);
return summarizeJsonResponses(outputs);
} finally {
pool.shutdown();
}
}
function printSummary(mode: string, totals: RenewalSummary, ms: number): void {
const seconds = Math.max(1e-9, ms / 1000);
const rps = REQUESTS / seconds;
console.log(mode);
console.log("requests :", REQUESTS.toLocaleString());
console.log("invalid rate :", `${INVALID_PERCENT}%`);
console.log("accepted :", totals.ok.toLocaleString());
console.log("renewed :", totals.renewed.toLocaleString());
console.log("rejected :", totals.rejected.toLocaleString());
console.log("output bytes :", totals.outputBytes.toLocaleString());
console.log("took :", `${ms.toFixed(2)} ms`);
console.log("throughput :", `${rps.toFixed(0)} req/s`);
}
async function main() {
const rawRequests = await buildDemoRevalidateRequests({
count: REQUESTS,
invalidPercent: INVALID_PERCENT,
});
const hostStart = performance.now();
const hostTotals = await runHost(rawRequests);
const hostMs = performance.now() - hostStart;
const workerStart = performance.now();
const workerTotals = await runWorkers(rawRequests);
const workerMs = performance.now() - workerStart;
const uplift = (hostMs / Math.max(1e-9, workerMs) - 1) * 100;
console.log("JWT token revalidation");
console.log(`threads: ${THREADS}`);
console.log("");
printSummary("host", hostTotals, hostMs);
console.log("");
printSummary("knitting", workerTotals, workerMs);
console.log("");
console.log(`uplift: ${uplift.toFixed(1)}%`);
}
if (isMain) {
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
}

HMAC verification and key derivation are CPU-bound. On a busy API server handling hundreds of authenticated requests per second, that crypto work competes with route handling on the main thread. Moving it to a pool keeps your event loop responsive — especially under mixed traffic where some routes are cheap and others hit the auth path hard.