API Timeout Troubleshooting Guide
Your API is returning 504s, requests are hanging, and users are complaining. Timeouts are difficult to debug because the cause can lurk anywhere in the chain — your code, your database, a third-party service, or your infrastructure.
This guide walks you through diagnosing and fixing the most common timeout causes.
Step 1: Identify the type of timeout
Not all timeouts are the same. The error you’re seeing reveals the failure point:
| Error | Status Code | What It Means |
|---|---|---|
ETIMEDOUT | — | Connection could not be established |
ESOCKETTIMEDOUT | — | Connection established but response took too long |
504 Gateway Timeout | 504 | Reverse proxy (Nginx, Cloudflare) gave up waiting |
408 Request Timeout | 408 | Server closed the connection due to idle client |
ERR_NETWORK / AbortError | — | Client (browser/fetch) cancelled the request |
// Check what you're actually getting
app.use((err, req, res, next) => {
if (err.code === 'ETIMEDOUT') {
console.error('Connection timeout to:', err.address);
} else if (err.code === 'ESOCKETTIMEDOUT') {
console.error('Read timeout — server connected but response too slow');
}
next(err);
});
504 Gateway Timeout is the most common in production. It means your reverse proxy or platform (Vercel, AWS ALB, Cloudflare) enforces a time limit that your endpoint exceeded.
Default timeout limits by platform:
| Platform | Default Timeout |
|---|---|
| Vercel (Hobby) | 10s |
| Vercel (Pro) | 60s |
| AWS ALB | 60s |
| Cloudflare | 100s |
| Nginx | 60s |
| Heroku | 30s |
Step 2: Trace the slow path
Before fixing anything, find the bottleneck. Add timing to your endpoint:
app.post('/api/generate-invoice', async (req, res) => {
const start = Date.now();
const timers = {};
timers.dbQuery = Date.now();
const orders = await db.orders.findMany({ where: { userId: req.userId } });
timers.dbQuery = Date.now() - timers.dbQuery;
timers.calculation = Date.now();
const invoice = calculateInvoice(orders);
timers.calculation = Date.now() - timers.calculation;
timers.pdfGeneration = Date.now();
const pdf = await generatePDF(invoice);
timers.pdfGeneration = Date.now() - timers.pdfGeneration;
timers.emailSend = Date.now();
await sendEmail(req.userId, pdf);
timers.emailSend = Date.now() - timers.emailSend;
console.log('Endpoint timing:', {
total: Date.now() - start,
...timers,
});
res.json({ invoiceId: invoice.id });
});
Common culprits:
- Unbounded database queries returning thousands of rows
- N+1 queries making hundreds of sequential database calls
- External API calls to slow or unresponsive services
- Heavy computation like PDF generation, image processing, or data aggregation on the server
- Missing connection pools creating a new connection per request
Step 3: Apply quick fixes for common causes
Missing connection pool
// Bad: new connection every request
app.get('/api/data', async (req, res) => {
const client = new Client(DATABASE_URL); // cold connection = 50-200ms
await client.connect();
const result = await client.query('SELECT ...');
res.json(result.rows);
});
// Good: reuse connections
const pool = new Pool({ connectionString: DATABASE_URL, max: 20 });
app.get('/api/data', async (req, res) => {
const result = await pool.query('SELECT ...');
res.json(result.rows);
});
Unbounded queries
// Bad: could return millions of rows
const users = await db.users.findMany();
// Good: always paginate
const users = await db.users.findMany({ take: 50, skip: offset });
Missing timeouts on outbound calls
// Bad: waits forever for third-party API
const data = await fetch('https://slow-api.example.com/data');
// Good: fail fast
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const data = await fetch('https://slow-api.example.com/data', {
signal: controller.signal,
});
clearTimeout(timeout);
Synchronous operations blocking the event loop
// Bad: blocks the entire Node.js process
const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512');
// Good: non-blocking
const hash = await crypto.pbkdf2(password, salt, 100000, 64, 'sha512');
Step 4: Offload slow operations to a background queue
If an operation inherently takes a long time (report generation, data exports, AI inference), no optimization will squeeze it into a 30-second window. Move it out of the request cycle:
// Before: synchronous, times out
app.post('/api/export', async (req, res) => {
const data = await aggregateAllUserData(req.userId); // 30-120s
const csv = await generateCSV(data); // 10-30s
await uploadToS3(csv); // 2-5s
res.json({ downloadUrl: csv.url }); // too late — already timed out
});
// After: instant response, background processing
app.post('/api/export', async (req, res) => {
const exportId = crypto.randomUUID();
await saveToDatabase({ id: exportId, status: 'pending', userId: req.userId });
await aq.tasks.create({
callbackUrl: 'https://your-app.com/api/run-export',
payload: { exportId, userId: req.userId },
webhookUrl: 'https://your-app.com/api/on-export-done',
retries: 2,
timeout: 300,
});
res.json({ exportId, status: 'pending' });
});
With AsyncQueue handling the slow work, your endpoint responds instantly while processing happens outside the request timeout window. Failed tasks get automatic retries.
Step 5: Set up monitoring and alerts
Once you’ve fixed the immediate issue, prevent future regressions:
// Track response times per endpoint
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
const route = req.route?.path || req.path;
if (duration > 5000) {
console.warn(`Slow endpoint: ${req.method} ${route} took ${duration}ms`);
}
metrics.histogram('http_request_duration_ms', duration, {
method: req.method,
route,
status: res.statusCode,
});
});
next();
});
Key metrics to track:
- p95 and p99 response times — averages mask tail latency
- Timeout rate — percentage of requests returning 504/408
- External API response times — track each third-party dependency separately
- Queue depth — if using background tasks, watch for backlog buildup
Quick Reference: Timeout Checklist
- Check the error code — is it a connection, read, or gateway timeout?
- Add timing logs to find the slow operation
- Look for missing connection pools, unbounded queries, and missing outbound timeouts
- If the operation is inherently slow, offload it to AsyncQueue
- Add response time monitoring to catch future slowdowns