Skip to main content

Fastify server not responding — triage guide

TL;DR

How to diagnose and fix a Fastify server that accepts connections but returns no response due to event loop blocking or plugin failures.

Key facts

Topic
Production error triage
Stack
Node.js / Linux

TL;DR

A Fastify server that accepts TCP connections but never sends a response is almost always suffering from a blocked event loop. Unlike a crash (where the port closes), the process stays alive but cannot process the request callback because the single thread is occupied with synchronous work. Clients see a hanging request that eventually times out.

Common causes

  • Synchronous operations on the main thread — CPU-heavy JSON parsing, image processing, or cryptographic operations blocking the event loop for hundreds of milliseconds or more
  • Plugin registration failures — a plugin throwing during the onReady hook leaves the server in a half-initialised state where routes are registered but handlers cannot execute
  • JSON schema compilation bottleneck — Fastify pre-compiles JSON schemas for validation and serialisation at startup. Deeply nested or circular schemas can cause the compilation phase to hang or consume excessive CPU
  • Thread pool exhaustion — libuv's default thread pool is 4 threads. If your application uses fs, DNS lookups, or crypto operations heavily, the pool saturates and async operations queue indefinitely
  • Unclosed database connections or streams — a handler awaiting a query on a connection pool with no available connections hangs without timing out

Diagnosis workflow

Confirm the process is alive and the port is open:

ss -tlnp | grep :3000
curl -v --max-time 5 http://localhost:3000/health

If curl connects but never receives a response, the event loop is blocked. Profile it:

npx clinic doctor -- node server.js

Clinic Doctor generates a report showing event loop delay, active handles, and CPU usage. A healthy Fastify server should show event loop delay under 20 ms.

For a production-safe flame graph without restarting:

npx 0x -p $(pgrep -f 'node.*server') -D 10000

This captures a 10-second flame graph of the running process, showing exactly which functions are consuming CPU time.

Check the thread pool

Increase the libuv thread pool if async operations are queuing:

UV_THREADPOOL_SIZE=16 node server.js

Monitor thread pool usage with the uv_metrics_info API or by checking the gap between DNS/fs callback scheduling and execution times.

Plugin registration debugging

Fastify's encapsulation model means a failing plugin can silently prevent child routes from loading. Enable verbose logging during startup:

const app = Fastify({ logger: { level: 'debug' } });

app.addHook('onReady', () => {
  app.log.info('All plugins loaded, server ready');
});

If onReady never fires, a plugin is hanging during registration. Bisect by commenting out plugins until the server starts.

Schema performance

If startup is slow, audit your JSON schemas:

app.after(() => {
  const schemas = app.getSchemas();
  console.log(`Registered schemas: ${Object.keys(schemas).length}`);
});

Avoid circular $ref schemas and deeply nested oneOf/anyOf combinators — these cause exponential compilation time in Ajv. Pre-compile schemas and cache the validators if you have more than 50 route schemas.

Where Reflex helps

Reflex monitors Fastify's event loop latency and HTTP response times. When response times spike or health checks time out, Reflex can capture diagnostic data, restart the affected process, verify the server responds within acceptable latency, and alert your team with the timeline and profiling data attached. See How it works.