Driver FixRecommendedSound, Wi-Fi or graphics acting up? Check drivers firstFind missing or outdated drivers fast.Check DriversOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PC×
Skip to content
World desk9 min

HTTP Request Hangs Forever in Production: Where Timeouts Actually Live in Node, Python, and Go

A timeout setting's name doesn't prove its scope. Learn what Node.js, Python Requests, and Go timeouts actually bound, which ones only notify, and how to make a deadline stop the work.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.Timeout is a whole-operation limit, but transport-level timeouts such as ResponseHeaderTimeout cover 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.requestTimeout limits 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.headersTimeout defaults to the minimum of 60,000 ms and requestTimeout.
  • 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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.Timeout is a total limit covering connection time, redirects, and reading the response body. The timer keeps running after Do returns, 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.ResponseHeaderTimeout starts 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Layers 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

  1. 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.
  2. 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.Duration in Go).
  3. Classify each timeout as connect, header wait, inactivity, or total, using the table above.
  4. Check that expiry cancels. In Node, confirm the handler destroys the request or an AbortSignal is attached and its error is handled. In Go, confirm the context reaches NewRequestWithContext and the body is closed. In Python, confirm timeout is present on every call, including ones inside helper libraries.
  5. Compare against requirements. If the need is a hard total limit, confirm the setting you found can actually express one.
  6. 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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Outdated Drivers Are Slowing You DownFree scan - exact matches
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.