A request that hangs “forever” almost always has a timeout somewhere. It just isn’t the timeout you assumed. The setting may cover only connection setup, only the wait for response headers, or only stretches of silence on a socket. It may also merely announce that time ran out without stopping anything. Each of the three runtimes covered here fails in its own way:
- Node.js core
http: a client socket timeout fires an event but does not abort the request. - Python Requests: there is no timeout unless you pass one, and the read timeout is an idle gap between bytes, not a cap on total download time.
- Go:
http.Client.Timeoutis a whole-operation limit, but transport-level timeouts such asResponseHeaderTimeoutcover only a slice of the exchange.
The question to ask of any stuck call is: which phase is stuck, which timer covers that phase, and what code actually cancels the work when the timer fires? The rest of this article answers it for each runtime.
As an Amazon Associate I earn from qualifying purchases.
Version basis: Node.js v26.10.0 HTTP documentation, Requests 2.34.2, Python 3.13.16 urllib.request, and the rolling Go net/http documentation, all as read on 2026-10-05. Check the versions you actually deploy before relying on any default quoted below.
Free tools Windows power users keep installed
One-click scans. No signup required.
Start with the phase, not the number
“Request duration” hides several distinct waits. A hang in one phase is invisible to a timer that watches another. Before changing any setting, work out where the request is parked:
#1 Best Overall
| Phase | What “stuck” looks like | Timestamp to log |
|---|---|---|
| DNS and TCP connect | No bytes ever leave; unreachable or filtered address | Request start, connect established |
| TLS handshake | Connected, but the handshake never completes | TLS complete |
| Request write | Large upload or a stalled request body | Request body fully sent |
| Waiting for response headers | Upstream accepted the request but is computing, queued, or wedged | First response header |
| Reading the body | Headers arrived, then the body trickles or stops | First body byte, body complete |
The client libraries do not all expose every one of these measurements directly, so you may need a custom transport, an event hook, or packet-level tooling to fill in gaps. Even a partial record (start, first header, body complete) is enough to tell a connection problem from a slow-body problem, and that determines which timeout you need.
How the timeouts compare
| Runtime / API | What the timeout controls | What it does not mean | Cancellation caveat |
|---|---|---|---|
Node.js http.ClientRequest.setTimeout() |
Socket timeout notification once the request is associated with a socket | It does not abort the request | Abort with an AbortSignal or destroy the request yourself, and handle the resulting error |
Python Requests timeout= |
A scalar applies to both connect and read; a tuple sets them separately | Read timeout is not total download time; it is the wait between bytes | Omitted means no timeout; None means wait without one |
Python urllib.request.urlopen(..., timeout=) |
Seconds for blocking operations such as the connection attempt; applies to HTTP, HTTPS, and FTP | The documentation does not present it as an application-wide operation deadline | Separate semantics from Requests; do not assume they match |
Go http.Client.Timeout |
Whole-request limit: connection, redirects, and reading the response body | Not just a header wait | Zero means no timeout; a request context can carry its own deadline or cancellation |
Go Transport.ResponseHeaderTimeout |
Wait for response headers after the request, including its body, has been fully written | Does not include reading the response body | Pair with a client timeout or context if you need a total bound |
Node.js: a timeout event is not an abort
What setTimeout() really does
Node’s http module is deliberately low-level: it streams messages and does not buffer whole responses for you. For outbound requests, the documentation states that setting the timeout option or calling request.setTimeout() does not abort the request. It only adds a 'timeout' event, tied to socket inactivity. If nothing listens for that event and acts on it, the request carries on holding its socket, memory, and whatever your code is awaiting.
That is the classic production shape in Node: a handler that logs “request timed out” yet the request keeps running, and the promise wrapping it never settles.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Wiring expiry to cancellation
Two documented mechanisms actually stop the work. You can destroy the request inside the 'timeout' handler, or pass an AbortSignal, which aborts an ongoing request and emits an error.
Rank #2
import http from 'node:http';
// Option 1: abort on a wall-clock deadline
const req = http.get(url, { signal: AbortSignal.timeout(5000) }, (res) => {
res.resume(); // consume or pipe the body
});
req.on('error', (err) => {
// AbortError / ECONNRESET end up here; reject your promise, log, release resources
});
// Option 2: act on the socket-inactivity notification
const req2 = http.get(url, (res) => res.resume());
req2.setTimeout(5000, () => req2.destroy(new Error('socket idle for 5s')));
req2.on('error', (err) => { /* same handling */ });
The two options are not equivalent. Option 1 is a wall-clock limit on the whole request. Option 2 fires on socket inactivity, so a peer that sends a byte every few seconds can keep it quiet indefinitely. Choose based on whether your requirement is “never longer than N seconds” or “never silent for N seconds”. Whichever you pick, always attach an 'error' listener; an abort or reset with no listener is a different kind of production incident.
Do not confuse these with server timeouts
Node’s server-side settings protect the server’s inbound side and do nothing for calls your process makes outward. Per the current documentation:
server.requestTimeoutlimits how long the server waits to receive the entire request. It defaults to 300,000 ms (five minutes), changed from no timeout in Node v18.0.0.server.headersTimeoutdefaults to the minimum of 60,000 ms andrequestTimeout.- The general server socket inactivity timeout defaults to zero, meaning disabled.
These are protections for the receiving server, not recommended application deadlines. If a client of your Node service hangs, these may explain it; if your Node service’s call to a dependency hangs, they are irrelevant.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Third-party Node HTTP clients layer their own timeout options on top of core http. Their semantics are not covered here, so read their documentation for what each option bounds.
Rank #3
Python: Requests waits forever unless told otherwise
The default is no timeout
The Requests documentation says requests do not time out unless you supply a value, and puts it bluntly: “Nearly all production code should use this parameter in nearly all requests.” A bare requests.get(url) can therefore block a worker until the operating system or something in the network path gives up. Passing timeout=None explicitly has the same meaning: wait without a timeout.
Scalar versus tuple
import requests
# One value applies to both connect and read
requests.get(url, timeout=5)
# Separate connect and read limits
requests.get(url, timeout=(3.05, 20))
The connect timeout bounds establishing the connection. The read timeout is the time spent waiting between bytes from the server. That has two consequences:
- A server that drips one byte inside each read interval never trips it. Total elapsed time can far exceed the read value, so it is not a cap on response duration.
- When a hostname resolves to several addresses, Requests attempts them in sequence. Observed total connection time can then exceed the single connect value you set.
When you need a true total deadline
Requests has no total-duration parameter. If the business rule is “this call must finish within 10 seconds”, the timeout argument alone cannot express it. Build the deadline at the operation level: for example, stream the body (stream=True), read it in chunks while checking a monotonic clock against your deadline, and abandon the response once it passes. A blocked read can still overshoot by up to the read timeout, so keep that value small relative to the budget. Alternatively, run the call under a supervisor that can enforce a deadline and discard the work. Whichever you choose, do not describe the read timeout as a total-response cap in code comments or runbooks, since someone will rely on it.
urllib.request.urlopen is a different API
The standard library’s urlopen takes an optional timeout in seconds for blocking operations such as the connection attempt, and the documentation says it applies to HTTP, HTTPS, and FTP. It is not the Requests connect/read pair, and its documentation does not describe it as an overall deadline. Python async HTTP clients are outside what is covered here; check their own documentation before assuming either behavior.
Rank #4
Go: strong total limit, but only if you set it
Client, transport, and context are separate layers
Go’s net/http lets you set limits at several levels, and mixing them up is the usual source of confusion.
http.Client.Timeoutis a total limit covering connection time, redirects, and reading the response body. The timer keeps running afterDoreturns, so a slow body read is still cut off. Zero means no limit, which is the default for a freshly created client, including the zero-value client.- Request context gives per-request deadlines and explicit cancellation, which is useful when different calls need different budgets or when a caller’s deadline should flow down into the outbound request.
Transport.ResponseHeaderTimeoutstarts only after the request, including its body, has been completely written, and it ends when headers arrive. It excludes reading the response body, so it cannot stand in for a total limit.
client := &http.Client{
Timeout: 15 * time.Second, // whole operation, including body reads
Transport: &http.Transport{
ResponseHeaderTimeout: 5 * time.Second, // header wait only
},
}
ctx, cancel := context.WithTimeout(parentCtx, 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { return err }
resp, err := client.Do(req)
if err != nil { return err }
defer resp.Body.Close()
Here the tightest applicable limit wins: the context deadline (10 s) ends the call before the client limit (15 s) would. Decide deliberately whether you need one whole-request budget, a header guard, or both, then keep client, transport, and context consistent so one does not silently dominate the others.
Body handling can masquerade as a pool problem
Callers must close the returned response body. To let a persistent connection be reused, the package documentation advises reading the body to EOF and closing it; skipping either can prevent reuse. Handlers that return early without draining the body leave connections unusable, which can resemble pool exhaustion or an unexplained stall. This is a resource-management issue rather than a timeout-scope issue, but it belongs in any Go hang investigation.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteLayers outside your code
Reverse proxies, load balancers, service meshes, and cloud platforms can each impose their own limits, and those are independent of the application timeouts above. No universal ordering or default holds across them, so check each one in your own deployment. What you can control is consistency: make sure the caller’s deadline is the one that expires first on purpose, rather than discovering that an intermediary cuts the connection at some other moment and surfaces it as a reset.
Retries share one budget
A retry loop multiplies waiting. Three attempts with a 10-second timeout each is a 30-second operation, plus any backoff sleeps. Define a single operation budget, derive each attempt’s timeout from the time remaining, and stop retrying once the caller’s deadline has passed. In Go, that falls out naturally if every attempt shares the parent context. In Node and Python you need to carry a deadline value through the loop yourself.
A debugging checklist for a hung request
- Capture phase timestamps (start, connect, TLS, request sent, first header, first body byte, body complete) for a failing request, even if only some are available.
- Find the client construction and the call site. Look for absent or zero timeouts, and confirm units (seconds in Python, milliseconds in Node’s options,
time.Durationin Go). - Classify each timeout as connect, header wait, inactivity, or total, using the table above.
- Check that expiry cancels. In Node, confirm the handler destroys the request or an
AbortSignalis attached and its error is handled. In Go, confirm the context reachesNewRequestWithContextand the body is closed. In Python, confirmtimeoutis present on every call, including ones inside helper libraries. - Compare against requirements. If the need is a hard total limit, confirm the setting you found can actually express one.
- Compare with intermediaries. Check proxy, load balancer, and platform limits against the application deadline, and make sure retries fit inside one budget.
This article draws on the official documentation of each runtime and did not involve hands-on load testing or reproduction of a production incident. Behavior of the versions you run may differ from those cited above.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →




