Pass signal: AbortSignal.timeout(8000) to the call and catch the TimeoutError: that is how to set a timeout on fetch in the browser and in current Node.js, and it is one line. Do it for every call that leaves your server. A stalled AI, email or payment provider can otherwise hold the request and its worker for minutes or indefinitely.

What counts as an external call, and what a timeout does to it

An external call is any request that leaves your process: the model, payment and email providers, and also your own database, auth provider and storage. A request timeout is the deadline the caller sets so it stops waiting and takes a planned path. It comes in three kinds, connect, read and total, and users feel the total.

Timeouts are one part of hardening SaaS applications for resilience, and the part that decides whether one slow provider stays a small problem. An external API, in the sense that matters here, is any service your app calls over the network but does not run: the model provider, Stripe, the email service, a maps or enrichment API, a customer’s webhook URL. For timeouts the list is wider, because the database, the auth provider, object storage and the cache also sit across a network. Some platforms use “External API” as a product name, for their own configured REST calls or for the API they open to outside tools; that product sense is not the subject here.

Kind of external callExamples in a small SaaSWhat the user sees when it hangs
Model providera chat reply, a summary, a generated imagea spinner that keeps turning, then a host error page
Payment providera checkout session, a refund, a plan changea pay button stuck on “processing”, so the user clicks again
Email providera sign-up confirmation, a password reseta form that never says the email went out
Data APIan address lookup, company enrichmenta field that never fills, blocking the form around it
Webhook targetthe app calling a customer’s URLa job that never finishes and never reports why
Your databaseevery page that reads or writespages with nothing to do with the slow query start failing
Auth providerthe session check on each requestevery signed-in page stalls at once
Storage and cacheuploads, cached lookupsan upload bar parked at the same percentage

A timeout is a deadline. When it passes, the caller stops waiting, lets go of the connection and runs the code you wrote for that case. Three deadlines matter, and clients name them differently and do not all offer each one: a connect deadline for opening the socket, a read or idle deadline for the gap between bytes, and a total deadline for the whole call. The total is the one a person staring at the screen experiences; the master table below shows where each client sets them. The deadline belongs in the client call or the client’s constructor. A spinner that gives up in the browser while the server keeps waiting fixes nothing on the server.

Deliverable 6.4 of the Production Hardening Sprint sets deliberate timeouts for AI, email, payment, and other external calls.

What goes wrong without it

Four failures come from a missing deadline, and each looks different from the user’s side.

What the user seesWhat is happening underneathWhich deadline was missing
The page spins until the user gives upthe server is still waiting on a provider that accepted the connection and went quieta total deadline on that call
The AI feature fails with a host error, not the app’s messagethe host’s limit ended the function before the SDK’s long default dida deadline shorter than the host’s limit
Unrelated pages start failingstuck calls hold workers and database connections until none are freedeadlines on every call that holds a connection
Duplicate charges, emails or jobsthe user clicked again and a second slow call started beside the firsta deadline plus a planned retry rule

An API request hangs forever with no timeout when nothing on the path is willing to give up: the browser waits for the server, the server waits for the provider, and the provider has stopped answering without closing the socket. The user sees a spinner. The server sees nothing at all, because no error was ever raised, so the logs stay clean while people leave.

A model API call that never returns fails in a second way: the SDK is still waiting when the host has already answered the user with an error. The OpenAI Python client’s README says “By default requests time out after 10 minutes.” On a serverless host the platform’s own clock can run out first. Vercel returns FUNCTION_INVOCATION_TIMEOUT as a 504 Gateway Timeout when a function exceeds its duration budget. Netlify’s synchronous functions are limited to 60 seconds. The error, its two clocks and the streaming fix have their own write-up: why an OpenAI API timeout makes the AI feature hang, then fail.

On a long-running server the damage spreads sideways. Every stuck call holds a worker, and often a database connection, so one slow provider starts failing pages that never call it. The pool errors that follow are listed in what each connection pool error means. The fourth failure is the retry storm: with no deadline, a user who clicks again starts the same slow call a second time, and neither one ends.

Across the 21 third-party apps I audited in June and July 2026, the Reliability & Correctness pillar averages 31.4 out of 100, scored on all 21. Those 21 are 11 public vibe-coded apps I audited exhaustively and 10 held-out apps I audited blind, a set I selected, not a random sample and not a rate for AI-built apps in general. The score covers the whole pillar, not timeouts alone, and I have no count of how many of those apps lacked deadlines.

How to set a timeout on fetch, in SDK clients and in the database driver

A timeout on fetch is one option: pass signal: AbortSignal.timeout(ms) and handle the TimeoutError. SDK clients for AI and payments take a timeout in their constructor, Python’s requests needs timeout= on every call, and Postgres takes a statement timeout. Each client’s default differs, so set every one of them yourself.

Every default below comes from the client’s own documentation, read on 30 September 2026, not from a test run for this page.

ClientWhere the deadline is setIts documented defaultSource, checked
fetch in the browsersignal: AbortSignal.timeout(ms) in the request optionsno timeout option among the request optionsMDN, 2026-09-30
fetch in Node.js (undici)the same signal; undici’s own headersTimeout, bodyTimeout, connectTimeoutundici’s Client options: 300e3 ms for headers and body, 10e3 ms to connectundici docs, 2026-09-30
OpenAI SDK, TypeScripttimeout on the client or per request10 minutesopenai-node README, 2026-09-30
OpenAI SDK, Pythontimeout option10 minutesopenai-python README, 2026-09-30
Claude SDK, TypeScripttimeout on the client or per request10 minutes; calculated dynamically for a large max_tokens without streaming, up to 60 minutesClaude docs, 2026-09-30
Stripe, Node.jstimeout in the config or per request80000 msstripe-node README, 2026-09-30
Python requeststimeout= on each call, one value or a (connect, read) tuplenone: requests do not time out unless a timeout is setrequests docs, 2026-09-30
Python httpxtimeout= per request or on the client; httpx.Timeout for connect, read, write and pool5 seconds of network inactivityhttpx docs, 2026-09-30
PostgreSQLstatement_timeout, idle_in_transaction_session_timeout0, which disables each; a value without units is millisecondsPostgreSQL 18 docs, 2026-09-30

fetch in the browser and in Node.js

According to MDN’s AbortSignal.timeout() reference, the signal “aborts with a TimeoutError DOMException on timeout”, which is how you tell a deadline apart from the AbortError a user’s cancel raises.

try {
  const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
  const data = await res.json();
} catch (err) {
  if (err.name === 'TimeoutError') {
    // the deadline fired: show the planned message, log the provider
  } else if (err.name === 'AbortError') {
    // canceled another way (Chrome 103 to 123 also reports a timeout like this)
  } else throw err;
}

MDN’s compatibility data lists AbortSignal.timeout() from Firefox 100, Safari 16, Chrome 124 and Node.js 17.3.0; Chrome 103 to 123 “Always aborts with an AbortError on timeout, not a TimeoutError”, which is why the AbortError branch stays. The older pattern, an AbortController with setTimeout(), does not depend on AbortSignal.timeout() at all, and MDN recommends it if resources are constrained and you want to cancel timeouts early, calling clearTimeout() when the operation finishes.

async function fetchWithDeadline(url, ms, options = {}) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);
  try {
    const res = await fetch(url, { ...options, signal: controller.signal });
    return await res.json(); // read the body before finally clears the timer
  } finally {
    clearTimeout(timer);
  }
}

To honor both a user’s cancel button and a deadline, pass AbortSignal.any([userController.signal, AbortSignal.timeout(8000)]), available from Chrome 116, Firefox 124, Safari 17.4 and Node.js 20.3.0.

Here is the distinction behind “fetch has no timeout”. The Fetch API’s request options include signal and list no timeout option, so in the browser the only deadline is the one you pass. Node’s built-in fetch is powered by undici, and undici’s Client options document their own defaults: headersTimeout and bodyTimeout of 300e3 milliseconds and connectTimeout of 10e3. Five minutes is longer than Netlify’s 60-second limit in the section above, so the signal is still yours to set.

SDK clients for AI, payments and email

SDKs take the deadline in the constructor or per request. The OpenAI and Claude TypeScript SDKs both expose timeout and maxRetries, and Stripe’s Node library has timeout plus its own maxNetworkRetries.

SDKTimeout optionRetry optionDocumented retry default
OpenAI, TypeScripttimeoutmaxRetries2; “requests which time out will be retried twice by default”
Claude, TypeScripttimeoutmaxRetries2; “requests that time out are retried twice by default”
Stripe, Node.jstimeoutmaxNetworkRetries1, for “failed requests that are safe to retry”

That retry column is where budgets break. With both AI SDKs left at their defaults, one call that keeps timing out can wait three full deadlines plus the backoff between them, so the deadline you set is not the wait the user gets. Set timeout and maxRetries together, on the client, where the next person will see them. Streaming long answers from OpenAI is covered in the OpenAI timeout article. Email providers are plain HTTP calls or thin SDKs over them: on a direct HTTP call the fetch signal above applies, and with an SDK, check its docs for a timeout or signal option.

Python: requests and httpx

requests’ timeout documentation is blunt: “By default, requests do not time out unless a timeout value is set explicitly.” A single timeout=5 covers both connect and read; a tuple such as (3.05, 27) sets them apart. The read timeout is the wait “between bytes sent from the server”, and “Neither the connect nor read timeouts are wall clock”. So a server that drips one byte at a time can hold a call with a timeout for far longer than the number suggests, which is one way a requests call with a timeout still hangs.

httpx timeouts are on by default, raising a TimeoutException after 5 seconds of network inactivity, split into connect, read, write and pool. Neither library documents a limit on the whole call: requests says its timeouts are not wall clock, and httpx’s four types are connect, read, write and pool. For a total deadline in async code, wrap the call in asyncio.timeout(), an asynchronous context manager added in Python 3.11 that cancels the task and raises TimeoutError when the time is up; synchronous code, such as a plain requests call, needs a limit at the worker level instead. Handling the resulting errors in Python is a separate topic.

The database and the auth provider are external calls too

One app I audited, a medical app, opened a brand-new set of database connections on every request instead of reusing a pool. It added latency every time, and the report expects a burst to exceed what Postgres accepts. The lesson I take: the database sits across a network like any provider, so it gets a pool, a deadline and a place in the request budget like any provider.

Postgres has its own deadlines, and their defaults are the Postgres row of the master table, from PostgreSQL’s client connection defaults. What to set them to, and why too low a value turns a slow page into a failing one, is covered in the connection pool article’s “Shorten the work that occupies each slot” section, and the pool’s own connection timeout is in that article’s error sections. Pool sizing belongs to connection pooling in Postgres.

The auth provider’s SDK is an HTTP call too. Middleware that validates a session on every request with no deadline turns an auth outage into a full outage. Give it a short deadline and decide in advance what happens when it fires: a cached session for read-only pages, or a clear sign-in prompt. The health endpoint must not hang on any of these either, which is part of what a health check endpoint is.

Third party API timeout checklist: choosing the numbers and what happens next

A third-party API timeout checklist has seven lines: every client has a deadline, connect and total are both set, the totals fit inside the platform’s function limit, retries are bounded and counted, payment retries reuse an idempotency key, the user sees a useful message, and every timeout is logged with the provider’s name.

One rule orders the numbers, and it is my working rule: the sum of every deadline and retry inside one request must be shorter than the platform’s own limit, so your error handling runs instead of the platform’s 504. Each plan’s limit is in Vercel function timeout limits and fixes. My starting points are a few seconds for an ordinary JSON API, a shorter connect deadline, and longer for model calls, then tuned from the latency you measure. They are where I begin, not a standard. Anything that needs more than the request allows moves off the request path, and how to run long tasks in the background is the next step for it.

Call typeStarting deadline (my working rule)On timeout the user seesRetry?Note
Ordinary JSON APIa few seconds total, shorter to connectthat card says “not available right now”; the page worksreads only, a small fixed number of timestune from measured latency
Model calllonger, with retries still inside the function limit”taking longer than usual”, with a try-again buttononly if repeating it is safe; count the SDK’s own retrieslong outputs go to a background job
Payment calla few seconds, inside the request budget”payment is being confirmed”, never “failed”only with the same idempotency keya timeout means unknown, not failed
Email senda few secondsthe action succeeds; the email followsyes, from a job, not the requestmove sending off the request path
Database queryseconds, through statement_timeoutan error state for that section onlyreads onlyvalues are the pool article’s
Auth session checkshorta sign-in prompt or a cached sessiononce at mostan auth outage should not take every page
  • Every client that leaves the process has an explicit deadline, not its default.
  • Connect and total deadlines are both set wherever the client separates them.
  • The deadlines and retries in one request add up to less than the function limit.
  • Retries are bounded, counted with the SDK’s own, and spaced with backoff.
  • A payment retry sends the same idempotency key as the first attempt.
  • The user gets a message that says what failed and whether to try again.
  • Each timeout writes a log line with the provider’s name and the elapsed time.

When a deadline fires, the next step is planned per row. The message says what failed and whether to retry, and the rest of the page keeps working, which is what graceful degradation is in practice. Retry only calls that are safe to repeat, and space the attempts with backoff, which is what exponential backoff is for. For payments, a timeout means the outcome is unknown, so a retry reuses the first key, which is the whole job of an idempotency key for safe retries; stripe-node’s own automatic retries add idempotency keys “where appropriate to prevent duplication”. How fast an API should be when nothing is wrong is a separate question, answered in what API response time an AI-built app needs.

How to verify it

External-call timeouts are verified in staging: simulate a slow API to test timeouts by pointing the app at an endpoint that sleeps longer than the deadline. The test passes on three measurements: the request returns within its budget of deadline and retries, the user sees the planned failure message, and no worker or database connection is left held.

There are three ways to build the slow endpoint, cheapest first. A small local server that accepts the connection and sleeps before answering, with a variant that sends headers and then stalls the body, covers any HTTP provider:

// slow-server.mjs: set DELAY_MS above your deadline; STALL=1 sends headers, then stalls
import http from 'node:http';

const delay = Number(process.env.DELAY_MS || 20000);
http.createServer((req, res) => {
  if (process.env.STALL) {
    res.writeHead(200, { 'content-type': 'application/json' });
    res.flushHeaders();
  }
  setTimeout(() => res.end('{"ok":true}'), delay);
}).listen(4010);

For a plain TCP dependency in staging, such as the database or the cache, Toxiproxy is “A TCP proxy to simulate network and system conditions”, with a latency toxic that adds delay and a timeout toxic that “Stops all data from getting through”. In front of an HTTPS provider it is awkward, because the client still checks the certificate against the host name it dialed, so the local server is the simpler route there. For a quick manual check, httpbin’s /delay/{delay} endpoint “Returns a delayed response (max of 10 seconds)”, which makes it useful only for deadlines under ten seconds. Test only endpoints you own or a service built for delay testing, never someone else’s production API.

These are my working steps, each with the evidence to keep:

  1. 01 Make every provider base URL configurable by environment variable, so staging can point at the slow endpoint without a code change. Evidence: the staging config showing the override.
  2. 02 Set the delay above the deadline and use the feature the way a customer would. Evidence: the delay value and the deadline side by side.
  3. 03 Pass when the request returns within its budget: the deadline times the attempts your retry setting allows, plus backoff and a small margin. An SDK left at its defaults retries a timed-out call, so count those attempts. Evidence: the measured timings and the retry setting.
  4. 04 Pass when the user sees the planned message and the rest of the page still works. Evidence: a screenshot.
  5. 05 Pass when the log shows the timeout line naming the provider, the error tracker shows it too if the app sends caught errors there, and no worker or database connection stays held. Evidence: the log line or event, and the pool counters or a second request that still gets a connection.
  6. 06 Repeat with the variant that sends headers and then stalls, which catches a client whose deadline covers only the connection. Evidence: the timing.

A handled timeout reaches an error tracker only when your code sends it there, which is why step five asks for both. The wider rehearsal, with a provider fully down instead of slow, is what chaos testing is.

In the Production Hardening Sprint, deliverable 6.4, external-call timeouts, is verified this way: simulate a slow dependency and confirm bounded waits and a useful failure state.

Where the sprint does this

Deliverable 6.4 is the work above, and three deliverables from the same area sit beside it. We add bounded retries with backoff to transient failures where repeating the operation is safe (6.5), keep unaffected functions usable when an external service is unavailable (6.8), and simulate AI or payment-provider failure in staging and verify the expected recovery behavior (6.10). Deliverable 3.11, AI feature hardening, includes for every model-backed feature a timeout with a fallback when the provider is down, and is verified this way: run injection test cases against each feature, a spend simulation that hits the limit, and a provider-outage simulation in staging. Your app’s current framework and hosting setup are our starting point; we refactor or replace components where the production work requires it. Deliverable 13.1, the production readiness report, delivers the result for every scope item, the work completed, and its verification evidence, and is verified this way: account for all 123 IDs; keep failures visible until resolved and explain genuine non-applicable items. Each one is listed in the published scope.

Common questions about request timeouts and external calls

What is the typical timeout for an API?

There is no standard one: documented client defaults include 5 seconds of network inactivity in httpx and 10 minutes in the OpenAI SDKs, and Python’s requests has none unless you set it. My working rule is to pick your own from the latency you measure and keep the total under your host’s function limit.

How long until an HTTP request timed out?

As long as the shortest deadline on its path, whether that belongs to the client, a proxy or the platform. MDN defines a 504 as a gateway or proxy that “did not get a response in time from the upstream server”, and a 408 as a server that “would like to shut down this unused connection”. On Vercel, the 504 is the platform ending your function, as described above.

What is external and internal API?

An external API is a service someone else runs, such as a model or payment provider; an internal API is one your own team runs, such as a second service or your own backend. Both sit across a network, so both need a deadline on the calling side.

How to use set timeout?

The JavaScript setTimeout timer does not cancel a request on its own; a request can be canceled by calling abort() on the AbortController whose signal was passed to it, so the timer has to make that call. AbortSignal.timeout() builds that pairing for you, and the older pattern clears the timer with clearTimeout() once the work finishes.

What happens when an API call fails?

Whatever your code planned for it: a bounded retry, a fallback, or a message that tells the user what failed. With no plan, the result is a stuck spinner or a blank screen, and the logs may show nothing because no error was ever raised.