logo

External services fail in ways you cannot predict. A payment API that responds in 2 seconds under normal load might take 30 seconds during peak traffic. A file conversion service might hang without limit. Your application should not break when this occurs.

This guide shows you how to build resilience around external dependencies that run slow, drop connections, or behave unpredictably.

Step 1: Identify Why External Service Calls Timeout

Timeouts happen when a response does not arrive within the expected window. Common causes:

CauseExample
Service overloadPayment API slow during Black Friday
Network congestionCross-region call hits routing delays
Cold startsThird-party service spinning up containers
Large payloadsVideo API processing a 2GB file
Downstream dependenciesThe service you call is waiting on its own backends
Rate limitingService throttles your request and delays the response

The key insight: you often cannot fix the external service. You can only control how your application handles the delay.

Step 2: Decouple the Request from the Wait

The most common mistake is making your user wait while you wait for the external service:

// BAD - user waits for everything
app.post('/api/create-order', async (req, res) => {
const inventory = await externalInventoryCheck(req.body); // 3s
const payment = await externalPaymentCharge(req.body); // 8s
const shipping = await externalShippingRate(req.body); // 5s
// User waited 16 seconds. Or got a timeout error.
res.json({ order: { inventory, payment, shipping } });
});

Instead, accept the request at once and process in the background:

// GOOD - user gets instant response
app.post('/api/create-order', async (req, res) => {
const { task } = await aq.tasks.create({
targetUrl: 'https://your-app.com/api/process-order',
payload: req.body,
timeout: 120,
maxRetries: 3,
onCompleteUrl: 'https://your-app.com/api/order-complete',
});
res.json({ orderId: req.body.orderId, taskId: task.id, status: 'processing' });
});

Your user sees a response in milliseconds. The external service calls happen in the background with proper timeout handling and retries.

Step 3: Use Retries with Backoff for Transient Failures

Many timeout failures are transient. The service was overloaded for a moment but recovers in seconds. Retries with exponential backoff handle this on autopilot.

await aq.tasks.create({
targetUrl: 'https://payment-api.example.com/charge',
payload: { amount: 99.99, customerId: 'cus_123' },
timeout: 30,
maxRetries: 3,
retryBackoff: 'exponential', // 1s, 2s, 4s between retries
});

Retry timeline:

Attempt 1: Call API -> timeout after 30s
Wait 1s
Attempt 2: Call API -> timeout after 30s
Wait 2s
Attempt 3: Call API -> success in 4s

Without retries, the first timeout would become a permanent failure. With retries, transient issues resolve on their own.

Tip: Only retry on 5xx errors and timeouts. Do not retry 4xx errors - those indicate a problem with your request that persists across attempts.

Step 4: Use Wait-for-Signal for Callback-Based Services

Some external services do not respond synchronously. They accept your request, process in the background, and notify you when finished. Payment processors, file converters, and identity verification services commonly follow this model.

For these, use waitForSignal:

// Start the process
const { task, signalToken } = await aq.tasks.create({
targetUrl: 'https://your-app.com/api/request-kyc-verification',
payload: { userId: 'user_456', documentUrl: 'https://...' },
waitForSignal: true,
maxWaitTime: 86400, // KYC can take up to 24 hours
timeout: 15, // The initial request should be fast
});
await db.verifications.create({
userId: 'user_456',
signalToken,
taskId: task.id,
});

When the KYC provider calls your webhook:

app.post('/api/kyc-callback', async (req, res) => {
const verification = await db.verifications.findByProvider(req.body.referenceId);
await fetch(`https://api.asyncqueue.io/v1/signals/${verification.signalToken}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
status: req.body.approved ? 'completed' : 'failed',
result: { decision: req.body.decision, details: req.body.details },
}),
});
res.json({ received: true });
});

This pattern eliminates timeout errors for callback-based services. Your initial request finishes fast (registering with the provider), and the result arrives whenever the provider completes its work.

Step 5: Set Appropriate Timeout Values

Generic timeouts (30 seconds for everything) cause two problems: they cut off genuinely slow services too early and let fast services hang too long before failing.

Measure first, then configure:

// Step 1: Log actual response times
const start = Date.now();
const result = await fetch(externalApi);
console.log(`External API took ${Date.now() - start}ms`);
// Step 2: Set timeout to 2-3x the 95th percentile
// If p95 is 8 seconds, set timeout to 20 seconds

Recommended approach by service type:

External ServiceRecommended TimeoutPattern
Payment charge30sFire-and-forget with retries
Payment with 3D Secure10s initial + 3600s waitWait-for-signal
File conversion (small)60sFire-and-forget with retries
File conversion (large)15s initial + 1800s waitWait-for-signal
Email API10sFire-and-forget
SMS API10sFire-and-forget
AI inference120sFire-and-forget with retries
Identity verification10s initial + 86400s waitWait-for-signal

The pattern: if the external service processes synchronously, use a generous HTTP timeout with retries. If it processes in the background and calls back, use wait-for-signal.