Python HTTPServer hangs: one idle connection blocks every request (use ThreadingHTTPServer)
A Python server built on http.server.HTTPServer can stop answering without
a traceback or a log line: requests hang until the client gives up
(curl: (28) Operation timed out), and then everything works again.
I reproduced this on my Mac. HTTPServer handles one connection at a time. If a
client opens a TCP connection and doesn’t send a request on it — for example a
browser’s speculative connection (preconnect) — the server waits on that connection,
and every other request queues behind it. (I simulated the idle client with a Python
socket below; I did not reproduce it with a browser.)
Reproduce it in two files
A minimal server. With --threading it uses ThreadingHTTPServer, otherwise the
plain HTTPServer:
# server.py
import sys
from http.server import HTTPServer, ThreadingHTTPServer, BaseHTTPRequestHandler
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body = b"ok\n"
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
server_class = ThreadingHTTPServer if "--threading" in sys.argv else HTTPServer
server_class(("127.0.0.1", 8765), Handler).serve_forever()
And a client that simulates a preconnect: it connects and then says nothing.
# preconnect.py
# Open a TCP connection and send nothing (simulates an idle preconnect).
import socket, time
s = socket.create_connection(("127.0.0.1", 8765))
time.sleep(30)
Run the plain server, make one normal request, then open the idle connection and try again:
$ python3 server.py &
$ curl -s -m 5 -w '%{http_code} %{time_total}s\n' http://127.0.0.1:8765/
ok
200 0.002656s
$ python3 preconnect.py &
$ sleep 0.5
$ curl -s -m 5 -w '%{http_code} %{time_total}s\n' http://127.0.0.1:8765/; echo "curl exit $?"
000 5.112399s
curl exit 28
$ kill %2 # close the idle connection
$ curl -s -m 5 -w '%{http_code} %{time_total}s\n' http://127.0.0.1:8765/
ok
200 0.001716s
The second request never got an answer. As soon as the idle connection went away, the server was healthy again — so the hang can look intermittent from the outside.
Where the server is stuck
While the request hangs, sample shows the server’s main thread blocked in a socket
recv (the full output is long; these are the lines around recv):
$ sample <server-pid> 1
...
776 sock_recv_into (in _socket.cpython-38-darwin.so) + 268
776 sock_call_ex (in _socket.cpython-38-darwin.so) + 368
776 sock_recv_impl (in _socket.cpython-38-darwin.so) + 36
776 __recvfrom (in libsystem_kernel.dylib) + 8
The stack doesn’t say which connection that recv is on. My reading: HTTPServer
accepts a connection and handles it to completion before calling accept() again,
and BaseHTTPRequestHandler.timeout is None by default, so “to completion” means
“until the client sends something or hangs up”. The idle connection was accepted
first, so the thread is most likely waiting for its request line, while curl waits
in the listen backlog. That fits the fact that closing the idle connection
immediately unblocked the server.
Fix 1: use ThreadingHTTPServer
http.server.ThreadingHTTPServer (Python 3.7+) is HTTPServer with
socketserver.ThreadingMixIn: one thread per connection, so an idle connection only
blocks its own thread. It is a one-word change:
Stop the previous server and the idle client first (Ctrl-C them, or kill the PIDs
the shell printed when you started them).
$ python3 server.py --threading &
$ python3 preconnect.py &
$ sleep 0.5
$ curl -s -m 5 -w '%{http_code} %{time_total}s\n' http://127.0.0.1:8765/; echo "curl exit $?"
ok
200 0.001708s
curl exit 0
Per the Python docs its daemon_threads is True, so threads stuck on idle connections
shouldn’t keep the process alive when you stop the server (I didn’t test that part).
python3 -m http.server already uses ThreadingHTTPServer (since 3.7), so the
command-line file server doesn’t show this problem — the same test returned 200
immediately. It bites when you write HTTPServer(...) in your own code, which is
what many examples and older snippets do.
Fix 2 (if you must stay single-threaded): a per-connection timeout
If the handler isn’t safe to run in parallel, at least stop one connection from
holding the server forever. Setting timeout on the handler applies a socket timeout
to each connection; an idle one is dropped after that many seconds:
# server_timeout.py
from http.server import HTTPServer, BaseHTTPRequestHandler
class Handler(BaseHTTPRequestHandler):
timeout = 2 # seconds to wait for a request line before dropping the connection
def do_GET(self):
body = b"ok\n"
self.send_response(200)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
HTTPServer(("127.0.0.1", 8765), Handler).serve_forever()
Stop the previous server and the idle client first (Ctrl-C them, or kill the PIDs
the shell printed when you started them).
$ python3 server_timeout.py &
$ python3 preconnect.py &
$ sleep 0.5
$ curl -s -m 5 -w '%{http_code} %{time_total}s\n' http://127.0.0.1:8765/; echo "curl exit $?"
ok
200 1.448683s
curl exit 0
The request now succeeds, but only after the idle connection’s timeout runs out
(the idle connection was opened about half a second before curl — the sleep 0.5
above — so the wait was ~1.5 s rather than the full 2 s).
With Python 3.12 the server’s stderr log shows the order of events — the idle
connection is dropped first, then curl’s request is served:
127.0.0.1 - - [02/Oct/2026 10:04:48] Request timed out: TimeoutError('timed out')
127.0.0.1 - - [02/Oct/2026 10:04:48] "GET / HTTP/1.1" 200 -
Every idle connection costs every other client up to timeout seconds — fine
for a script, not for a page that loads several resources. Prefer Fix 1; you can
combine both.
Checklist
- A Python HTTP server that intermittently stops answering, with no error in its log:
check whether it’s
HTTPServer(single connection at a time). - Reproduce by opening a TCP connection that sends nothing, then making a request.
- Switch to
ThreadingHTTPServer— same constructor, same handler. - If you need serial handling, set
timeouton the handler so an idle connection can’t hold the server forever. sample <pid>showing the server’s only thread blocked inrecvis a strong hint.
Tested on macOS 26.5.2 (Apple silicon) with Python 3.8.2 (/usr/bin/python3,
Command Line Tools) and Python 3.12.13 (Homebrew), curl 8.7.1. Outputs above are
from 3.8.2 unless noted; the server log lines were checked on 3.12. Server bound to 127.0.0.1
only. The idle client was a Python socket, not a browser.