The outbound fetch call with no timeout that takes your API down with it
Von Seaworthy · 8. August 2026
Photo by Nathan Dumlao on Unsplash
Somewhere in your API there's a call to a third-party service. Payment provider, geocoding API, an internal microservice, doesn't matter which. It looks like this:
const res = await fetch(`https://api.partner.com/lookup/${id}`)
Nothing wrong with it in the demo. The partner API responds in 80ms, your endpoint returns in under 100ms, and you move on. Then one day their API gets slow, or their load balancer starts dropping connections without closing them, and your fetch call just sits there. Not erroring. Waiting.
Node's built-in fetch has no default timeout. Neither does axios, neither does most Ruby or Python HTTP clients out of the box. The request hangs until the underlying TCP connection times out, which on most systems is somewhere between two and fifteen minutes depending on the OS and network stack. Your handler is stuck for that whole window, holding a request slot, a database connection if you opened one earlier in the handler, and a spot in whatever concurrency limit your process has.
Do this on a handful of requests and it's a minor annoyance. Do it under real traffic and it's an outage. Every incoming request that hits the slow code path piles up behind the ones already waiting. Your process runs out of file descriptors or your load balancer marks the instance unhealthy, and a single degraded dependency has taken down endpoints that have nothing to do with it.
Why a try/catch doesn't help
A try/catch only helps once the promise resolves or rejects. If the request is hanging, there's nothing to catch yet, since nothing has failed. You need something that forces a rejection after a bounded amount of time, independent of what the remote server does.
Setting a timeout
Node's fetch accepts an AbortSignal, and AbortSignal.timeout() is the least code you'll write to fix this:
async function fetchWithTimeout(url, ms = 5000) {
try {
return await fetch(url, { signal: AbortSignal.timeout(ms) })
} catch (err) {
if (err.name === 'TimeoutError') {
throw new Error(`Request to ${url} timed out after ${ms}ms`)
}
throw err
}
}
Five seconds is a reasonable default for most internal calls. Pick a number based on what your own endpoint's response budget actually allows, not what feels generous. If your handler has to respond in under a second to keep its own caller happy, a five-second timeout on a downstream call is already too slow. You've moved the failure instead of preventing it.
The database pool has the same problem
Connection pools fail the same way in the other direction. pg, mysql2, and most ORMs let you configure a connectionTimeoutMillis and statementTimeout separately from each other. If neither is set, a query stuck behind a lock can hold a pool connection indefinitely, and every other request waiting on that pool queues up behind it. Same failure shape as the fetch call, one layer down.
Picking a number isn't the hard part, having one is
I've seen plenty of code where someone adds a timeout to the one call that caused last month's incident and leaves every other outbound call in the same file untouched. That fixes the specific page you got, not the pattern that caused it. If your service talks to five external dependencies and only one of them has a timeout, the other four are next quarter's incident. Grep your codebase for fetch(, axios.get(, axios.post(, and your ORM's query methods, and check that each one has an upper bound on how long it will wait.
What a timeout doesn't fix
A timeout stops one slow dependency from cascading into a full outage, but the request still fails from the caller's point of view. If that call is on a critical path, pair the timeout with a retry policy that has its own backoff, and consider whether the endpoint can degrade gracefully instead of failing outright, returning cached or partial data rather than a 500. That's separate work, but it only makes sense once the timeout exists. Without it, retries just multiply the number of hung connections.
Seaworthy flags outbound HTTP calls and database queries with no timeout configured, since it's one of the more common ways a healthy-looking service degrades under real traffic instead of local testing.
Dieser Artikel wurde von KI generiert und fasst öffentlich verfügbare Quellen zusammen.
Seaworthy scannt dein Repository nach den Problemen, die in Artikeln wie diesem behandelt werden.
Sicherheitslücken, offengelegte Geheimnisse und Fehlkonfigurationen — erkannt, bevor du bereitstellst. Kostenlos, kein Konto nötig.