Timeouts
Rezo supports both simple timeouts (a single number) and staged timeouts that apply different limits to each phase of the HTTP request. Staged timeouts let you fail fast on connection issues while allowing more time for slow response bodies.
Simple Timeout
Pass a number (milliseconds) to set a single timeout for the entire request:
import { Rezo } from 'rezo';
// 10 second timeout for the entire request
const { data } = await rezo.get('https://api.example.com/data', {
timeout: 10000
});
// Instance-level default
const client = new Rezo({
timeout: 5000
}); When a simple timeout is provided, Rezo internally converts it into staged timeouts using parseStagedTimeouts():
| Phase | Value |
|---|---|
connect | min(timeout, 10000) — capped at 10 seconds |
headers | min(timeout, 30000) — capped at 30 seconds |
total | The full timeout value |
So timeout: 60000 becomes { connect: 10000, headers: 30000, total: 60000 }.
Staged Timeouts
For fine-grained control, pass a StagedTimeoutConfig object with separate timeouts for each request phase:
const { data } = await rezo.get('https://api.example.com/large-file', {
timeout: {
connect: 5000, // 5s to establish TCP connection
headers: 10000, // 10s to receive response headers
body: 120000, // 2min to receive the full body
total: 150000 // 2.5min maximum total duration
}
}); StagedTimeoutConfig
interface StagedTimeoutConfig {
/** Timeout for TCP connection establishment (milliseconds) */
connect?: number;
/** Timeout for receiving response headers after connection (milliseconds) */
headers?: number;
/** Timeout for receiving the full response body (milliseconds) */
body?: number;
/** Maximum total duration for the entire request (milliseconds) */
total?: number;
} All phases are optional. Omit a phase to skip its timeout entirely.
Phases Explained
connect
The time allowed to establish a TCP connection to the server (and complete the TLS handshake for HTTPS). If the server is unreachable, firewalled, or slow to respond to SYN packets, this timeout fires.
timeout: { connect: 3000 } // Fail fast if server is unreachable Error code: ETIMEDOUT Retryable: Yes — connection timeouts are almost always transient.
headers
The time allowed to receive the first response headers after the connection is established and the request is sent. Fires when the server accepts the connection but takes too long to start responding.
timeout: { headers: 15000 } // Server must start responding within 15s Error code: ESOCKETTIMEDOUT Retryable: Yes — header timeouts usually indicate a temporarily overloaded server.
body
The time allowed to receive the complete response body after headers arrive. Useful for large downloads where you want a generous body transfer time but a strict initial response time.
timeout: {
connect: 5000,
headers: 10000,
body: 300000 // 5 minutes for a large file download
} Error code: ECONNRESET Retryable: No — a body timeout means partial data was received and the transfer stalled. The server state may have changed, so retrying could produce inconsistent results.
total
The absolute maximum duration for the entire request, from initiation to completion. Acts as a safety net that overrides all other phase timeouts.
timeout: { total: 60000 } // Never spend more than 60s on this request Error code: ECONNRESET Retryable: No — the total timeout is a hard cap. The request may have been partially processed.
StagedTimeoutManager
The StagedTimeoutManager class manages phase timers internally. The HTTP adapter creates one per request and drives it through the lifecycle:
- Request starts:
startPhase('connect')andstartPhase('total')begin - Socket connects:
clearPhase('connect'), thenstartPhase('headers') - Headers received:
clearPhase('headers'), thenstartPhase('body') - Response complete:
clearAll()cancels all remaining timers - Timeout fires: Socket and request are destroyed,
AbortControlleris aborted
// Internal flow (for understanding, not direct usage):
const manager = new StagedTimeoutManager(
{ connect: 5000, headers: 10000, body: 60000, total: 90000 },
config,
requestConfig
);
manager.setSocket(socket);
manager.setRequest(req);
manager.setAbortController(controller);
manager.setTimeoutCallback((phase, elapsed) => {
// Handle timeout
});
manager.startPhase('connect');
manager.startPhase('total');
// ... on socket connect:
manager.clearPhase('connect');
manager.startPhase('headers');
// ... on headers received:
manager.clearPhase('headers');
manager.startPhase('body');
// ... on response complete:
manager.clearAll(); Timeout Errors
Timeout errors are RezoError instances with additional properties:
try {
await rezo.get(url, { timeout: { connect: 1000 } });
} catch (error) {
if (error.code === 'ETIMEDOUT') {
console.log(error.phase); // 'connect'
console.log(error.elapsed); // actual milliseconds elapsed
console.log(error.isRetryable); // true for connect/headers, false for body/total
console.log(error.message); // 'Connection timeout: Failed to establish TCP connection within 1000ms'
}
} Phase-Specific Error Messages
| Phase | Error Code | Message | Retryable |
|---|---|---|---|
connect | ETIMEDOUT | Connection timeout: Failed to establish TCP connection within Xms | Yes |
headers | ESOCKETTIMEDOUT | Headers timeout: Server did not send response headers within Xms | Yes |
body | ECONNRESET | Body timeout: Response body transfer stalled for Xms | No |
total | ECONNRESET | Total timeout: Request exceeded maximum duration of Xms | No |
Retryability
The isRetryable property is set on timeout errors to guide the retry system:
- Connect and headers timeouts are retryable because the server likely never processed the request, making it safe to retry.
- Body and total timeouts are not retryable because the server may have already processed the request and started sending data. Retrying could result in duplicate operations.
The retry system checks isRetryable when retryOnTimeout: true is configured. You can use it in custom retry conditions:
retry: {
maxRetries: 3,
condition: (error, attempt) => {
// Only retry if the error is marked retryable
return error.isRetryable === true;
}
} parseStagedTimeouts()
This utility function normalizes timeout configuration from either format into a StagedTimeoutConfig:
import { parseStagedTimeouts } from 'rezo';
// From a number
parseStagedTimeouts(30000);
// { connect: 10000, headers: 30000, total: 30000 }
// From an object (passed through as-is)
parseStagedTimeouts({ connect: 5000, headers: 10000, body: 60000 });
// { connect: 5000, headers: 10000, body: 60000 }
// From undefined
parseStagedTimeouts(undefined);
// {} Practical Examples
Fast API Client
Short timeouts for a responsive API that should always reply quickly:
const api = new Rezo({
timeout: {
connect: 2000,
headers: 5000,
total: 10000
},
retry: {
maxRetries: 2,
retryOnTimeout: true,
backoff: 'exponential'
}
}); Large File Download
Generous body timeout for slow transfers, strict connection timeout:
const response = await rezo.get('https://cdn.example.com/large-file.zip', {
timeout: {
connect: 5000,
headers: 10000,
body: 600000, // 10 minutes for the body
total: 660000 // 11 minutes total
}
}); Microservice Health Check
Ultra-fast timeout for health check endpoints:
async function isHealthy(serviceUrl: string): Promise<boolean> {
try {
await rezo.get(`${serviceUrl}/health`, {
timeout: { connect: 1000, headers: 2000, total: 3000 }
});
return true;
} catch {
return false;
}
} Different Timeouts Per Environment
const timeout = process.env.NODE_ENV === 'production'
? { connect: 3000, headers: 10000, body: 60000, total: 90000 }
: { connect: 1000, headers: 3000, total: 5000 }; // Strict in dev
const client = new Rezo({ timeout }); Interaction with Other Features
Retry System
When retries are enabled and a retryable timeout occurs, the request is automatically retried:
const { data } = await rezo.get(url, {
timeout: { connect: 2000, headers: 5000 },
retry: { maxRetries: 3, retryOnTimeout: true }
});
// If connect or headers timeout occurs, retries up to 3 times
// If body timeout occurs, does NOT retry (isRetryable = false) onTimeout Hook
The onTimeout hook fires when any timeout occurs, providing visibility into timeout patterns:
const client = new Rezo({
timeout: { connect: 5000, headers: 15000 },
hooks: {
onTimeout: [
(event, config) => {
metrics.increment('http.timeout', {
phase: event.type,
url: event.url,
configuredMs: event.timeout,
actualMs: event.elapsed
});
}
]
}
}); AbortController
Staged timeouts work alongside AbortController. When a timeout fires, the abort controller is signaled, which cancels the request cleanly:
const controller = new AbortController();
// External abort and timeout racing
const { data } = await rezo.get(url, {
signal: controller.signal,
timeout: { connect: 5000, headers: 10000, total: 30000 }
});
// External abort takes priority if it fires first
setTimeout(() => controller.abort(), 2000);