Next.js — boot warmup

Lazy pools are right for development, but production cold starts shouldn't spawn workers and seed a million records inside the first request. Next.js's instrumentation.ts hook is the answer: register() runs once when the Node.js runtime boots, before the server accepts traffic.

register()

src/instrumentation.tsbackend
// src/instrumentation.ts — Next.js runs register() once at server boot
export async function register() {
  if (process.env.NEXT_RUNTIME !== 'nodejs') return;
  const { getDigest } = await import('./app/api/atoll/pool');
  const { getJobs } = await import('./app/api/jobs/pool');
  const { getIncidentsApi } = await import('./app/api/incidents/pool');
  getDigest();
  getJobs();
  // Kick the expensive part: 1M records seed on workers during startup,
  // then a metrics recompute so the read-model endpoints are fully warm.
  void getIncidentsApi().client.seedIncidents()
    .then(() => getIncidentsApi().client.computeMetrics())
    .catch(console.error);
}

Three details matter: the NEXT_RUNTIME === 'nodejs' guard keeps it out of the edge runtime; the dynamic imports keep node:worker_threads out of bundle graphs that can't support it; and the seed is fired without await — boot doesn't block on it, it just starts early. Because the seed is dispatched at boot, read-model endpoints report live seedProgress during warmup and are fully warm before most traffic arrives.

Lifecycle

Pool identity lives on globalThis — in dev, module reloads re-run the getters but return the same pool instead of re-spawning workers on every HMR cycle. In production the pool outlives every request and dies with the process; for tests, terminate explicitly rather than leaking threads:

test/apiJobs.test.tsbackend
// Tests own their pool lifecycle — terminate so vitest can exit.
const holder = globalThis as { __atollJobs?: { pool: { terminate(): void } } };
afterAll(() => {
  holder.__atollJobs?.pool.terminate();
  delete holder.__atollJobs;
});

pool.terminate() also exists for graceful shutdown in a custom server — see custom server for the production topology that owns the process lifecycle.

What to warm

Anything whose first-call cost matters: worker spawn (~tens of ms each), shared-buffer initialization, and seed/refresh dispatches. Warming is cheap insurance under load — a cold pool under a request burst spawns while queuing requests, compounding latency exactly when you can least afford it.