onSend Hook — Mutate the Outgoing Payload

Add Headers, Wrap Responses, Sign Cookies

onSend Hook — Mutate the Outgoing Payload

onSend runs after serialization, just before bytes hit the socket — the right place for response wrappers, header injection, and signing.

4 min read Level 3/5 #fastify#hooks#onSend
What you'll learn
  • Use onSend to inject headers like X-Trace-Id
  • Wrap successful payloads in an envelope
  • Avoid heavy CPU work in onSend

onSend is the last hook before Fastify writes the response. The serialized payload is already prepared; you can swap it, append headers, or audit what’s leaving the server.

Add a Trace Header

app.addHook('onSend', async (req, reply, payload) => {
  reply.header('X-Trace-Id', req.id);
  return payload; // return the same payload to leave it untouched
});

Return the original payload (a string, Buffer, or stream) to leave the body unchanged. Return a new value to replace it.

Envelope Successful Responses

app.addHook('onSend', async (req, reply, payload) => {
  if (reply.statusCode >= 200 && reply.statusCode < 300 && typeof payload === 'string') {
    const body = JSON.parse(payload);
    return JSON.stringify({ ok: true, data: body, requestId: req.id });
  }
  return payload;
});

If you do this, declare a response schema that matches the envelope — otherwise fast-json-stringify won’t help and the parse/stringify round-trip is wasted work.

Be Careful With Streams

When payload is a stream, you cannot inspect it without consuming it. Either skip stream responses with an early return or pipe through a transform that re-emits.

Don’t Block

onSend runs synchronously on the request hot path. Anything CPU-heavy (compression, big-payload JSON manipulation) belongs in middleware that streams the work, or in a worker. Cookie signing and small header additions are fine.

Pair With onResponse

onSend modifies; onResponse fires after the response is fully written (great for metrics). Use them together — one to shape, one to observe.

onError Hook →