Socket Telemetry
Rezo’s HTTP adapter instruments every Node.js socket it touches and exposes the data through two complementary surfaces:
- Lifecycle hooks —
onDns,onSocket,onTls,onTimeout,onAbort— fire-and-forget arrays you register on the instance. Use these for metrics, logging, and live observability. getSocketTelemetry(socket)— read the timing breakdown for any active or completed socket directly. Use this when you have a reference to the socket (e.g. inside a hook).
Both are exposed by the HTTP adapter and the HTTP/2 adapter. The Fetch / XHR / cURL adapters cannot instrument sockets directly, so onDns / onSocket / onTls do not fire there.
Lifecycle Hooks
Hooks are configured on the instance via the hooks option. Each hook is an array of handler functions, called in order. They never throw into your request flow — uncaught errors are swallowed and logged.
onDns
Fires once per DNS resolution. Useful for tracking lookup latency, multi-region health, or DNS rebind monitoring.
import { Rezo } from 'rezo';
const client = rezo.create({
hooks: {
onDns: [
(event, config) => {
// event: { hostname, address, family (4|6), duration (ms), timestamp }
metrics.histogram('http.dns_lookup_ms', event.duration, {
host: event.hostname,
family: event.family
});
}
]
}
}); onSocket
Fires for every TCP-level socket event: connect, close, drain, error, timeout, end. Each event includes endpoint addresses and byte counts.
const client = rezo.create({
hooks: {
onSocket: [
(event, socket) => {
// event.type ∈ 'connect' | 'close' | 'drain' | 'error' | 'timeout' | 'end'
if (event.type === 'connect') {
console.log(`TCP ${event.localAddress}:${event.localPort} → ${event.remoteAddress}:${event.remotePort}`);
}
if (event.type === 'close') {
console.log(`socket closed — ${event.bytesWritten} sent, ${event.bytesRead} received`);
}
}
]
}
}); The second argument is the live Socket | TLSSocket. Inspect or instrument it however you like, but treat it as read-only — the adapter still owns its lifecycle.
onTls
Fires once per TLS handshake. Includes the negotiated protocol, cipher, the server certificate, and whether the chain validated.
const client = rezo.create({
hooks: {
onTls: [
(event, _socket) => {
// event: { protocol, cipher, authorized, authorizationError?,
// certificate?: { subject, issuer, validFrom, validTo, fingerprint },
// duration, timestamp }
if (!event.authorized) {
console.warn(`TLS not authorized for ${event.certificate?.subject}: ${event.authorizationError}`);
}
console.log(`TLS ${event.protocol} ${event.cipher} (${event.duration} ms)`);
}
]
}
}); onTimeout
Fires when any kind of timeout trips: connect, request, response, socket, or lookup. The event tells you which one and how long elapsed.
hooks: {
onTimeout: [
(event, _config) => {
alerting.warn(`${event.type} timeout for ${event.url} after ${event.elapsed} ms`);
}
]
} onAbort
Fires when a request is aborted — by user signal, by timeout, or by an internal error.
hooks: {
onAbort: [
(event, _config) => {
// event.reason ∈ 'user' | 'timeout' | 'signal' | 'error'
console.log(`aborted (${event.reason}) — ${event.url} after ${event.elapsed} ms`);
}
]
} Reading Telemetry Off a Socket
Inside onSocket (or any place you have a Socket reference) you can pull the full timing breakdown:
import rezo from 'rezo';
import { getSocketTelemetry } from 'rezo/adapters/http';
const client = rezo.create({
hooks: {
onSocket: [
(event, socket) => {
if (event.type !== 'close') return;
const telemetry = getSocketTelemetry(socket);
if (!telemetry) return;
// telemetry.timings is a one-shot breakdown captured per socket:
// { created, dnsEnd, dnsDuration, connectEnd, tcpDuration,
// secureConnectEnd, tlsDuration, address, family }
// telemetry.tls has { protocol, cipher, authorized, certificate? }
// telemetry.reuse has { count, lastUsed, isReused }
// telemetry.network has { remoteAddress, remotePort, localAddress, localPort, family }
metrics.histogram('http.dns_ms', telemetry.timings.dnsDuration ?? 0);
metrics.histogram('http.tcp_ms', telemetry.timings.tcpDuration ?? 0);
metrics.histogram('http.tls_ms', telemetry.timings.tlsDuration ?? 0);
}
]
}
}); For request-level TTFB / total time, read response.config.timing after the request resolves (see the next section) — those live on the per-request timing ladder, not on the socket itself.
When a socket is reused (keep-alive), the per-request DNS/TCP/TLS times in the request-level timing are all 0, because no new connection was established — that’s the cleanest way to confirm pooling is working from the response side.
End-to-End Timing on the Resolved Config
After a request resolves, the W3C-style timestamp ladder is preserved on response.config.timing for downstream analysis:
const response = await client.get('https://api.example.com/users');
const t = response.config.timing;
// t.startTime, t.domainLookupStart / End,
// t.connectStart, t.secureConnectionStart, t.connectEnd,
// t.requestStart, t.responseStart, t.responseEnd
const dnsMs = t.domainLookupEnd - t.domainLookupStart;
const tcpMs = t.connectEnd - t.connectStart;
const tlsMs = t.secureConnectionStart > 0
? t.connectEnd - t.secureConnectionStart
: 0;
const ttfbMs = t.responseStart - t.requestStart;
const totalMs = t.responseEnd - t.startTime; These match PerformanceResourceTiming semantics, which makes them easy to forward to RUM / OTel collectors.
Connection Reuse Detection
Two ways to confirm pooling:
getSocketTelemetry(socket).reuse.isReused→ boolean (most direct).- Watch the timestamps —
domainLookupStart === domainLookupEndandconnectStart === connectEndmeans the socket was reused.
hooks: {
onSocket: [
(event, socket) => {
if (event.type !== 'connect') return;
const telemetry = getSocketTelemetry(socket);
if (telemetry?.reuse.isReused) {
metrics.increment('http.socket_reuse');
} else {
metrics.increment('http.socket_new');
}
}
]
} Debug Mode
A quick alternative when you just want it printed to stderr:
const { data } = await rezo.get('https://api.example.com/data', {
debug: true
});
// [Rezo] DNS api.example.com → 93.184.216.34 (12 ms)
// [Rezo] TCP connected (45 ms)
// [Rezo] TLS TLSv1.3 (89 ms)
// [Rezo] TTFB 210 ms
// [Rezo] 200 OK in 350 ms debug: true is per-request; for instance-wide debug logging, set it in the constructor defaults.