Repository navigation
sync client: intermittent handshake timeout with TLS 1.3 session tickets #1779
Description
Checklist
[* ] I searched the FAQ and didn't find an answer.
[* ] I search issues and didn't find an earlier report.
[* ] I personally encountered this problem. I can explain the symptoms and the consequences without relying on an AI coding agent.
[* ] I wrote a short, specific issue description that makes it easy for maintainers to understand the problem and the consequences.
[* ] I am aware that maintainers have AI coding agents. Merely throwing an AI at an issue wastes their time. It comes across as a lack of respect.
Problem
The threading client (websockets.sync.client.connect) sometimes fails the opening handshake against a TLS 1.3 server that sends session tickets, which is the OpenSSL default. The TLS handshake completes, but the server never receives the HTTP upgrade request. The server logs connection closed while reading HTTP request line with 0 bytes read. On the client:
- usually
TimeoutErrorafteropen_timeout; - sometimes
ConnectionClosedError: no close frame received or sent
We hit this in the test suite of a service with mutual-TLS WebSocket listeners. About 1–2% of the threading client's connections failed under load, which made tests fail at random. Our production client uses websockets.asyncio.client and is not affected.
- Server with
ssl_context.num_tickets = 0: no failures. (OP_NO_TICKETis not enough: OpenSSL then still sends tickets, in stateful form.) - Client pinned to TLS 1.2: no failures (0 / 1000 in our own setup).
- asyncio client against the same server: no failures.
Possibly related: AssemblyAI/assemblyai-python-sdk#265 (a workaround in their SDK) and python/cpython#137583 (one thread blocked in SSLSocket.recv() while another sendall()s on the same socket).
Reproduction
Software stack:
- websockets 17.2
- Python 3.12.3
- OpenSSL 3.0.13
The script below produces the following output when ran with 100 attempts:
client=sync tickets=True n=100 failed=10 {'ConnectionClosedError <- SSLEOFError': 1, 'TimeoutError': 9}
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
-keyout key.pem -out cert.pem -days 2 -subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost"
python repro.py 100 sync tickets # some failures
python repro.py 100 sync notickets # 0
python repro.py 100 asyncio tickets # 0import asyncio, logging, multiprocessing, ssl, sys
from collections import Counter
N, CLIENT, TICKETS = int(sys.argv[1]), sys.argv[2], sys.argv[3] == "tickets"
PORT = 8765
def server(ready):
logging.getLogger("websockets").setLevel(logging.CRITICAL)
from websockets.asyncio.server import serve
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.minimum_version = ssl.TLSVersion.TLSv1_3
ctx.load_cert_chain("cert.pem", "key.pem")
if not TICKETS:
ctx.num_tickets = 0
async def handler(ws):
await ws.close()
async def main():
async with serve(handler, "localhost", PORT, ssl=ctx):
ready.set()
await asyncio.Future()
asyncio.run(main())
def connect_once(uri, ctx):
if CLIENT == "sync":
from websockets.sync.client import connect
with connect(uri, ssl=ctx, open_timeout=5):
pass
else:
from websockets.asyncio.client import connect
async def go():
async with connect(uri, ssl=ctx, open_timeout=5):
pass
asyncio.run(go())
if __name__ == "__main__":
ready = multiprocessing.Event()
p = multiprocessing.Process(target=server, args=(ready,), daemon=True)
p.start(); ready.wait(10)
ctx = ssl.create_default_context(cafile="cert.pem")
failures = Counter()
for _ in range(N):
try:
connect_once(f"wss://localhost:{PORT}/", ctx)
except Exception as exc:
cause = exc.__cause__
failures[type(exc).__name__ + (f" <- {type(cause).__name__}" if cause else "")] += 1
p.terminate()
print(f"client={CLIENT} tickets={TICKETS} n={N} failed={sum(failures.values())} {dict(failures)}")
Activity
aaugustin commentedon Oct 14, 2026
Yes, test suites are vulnerable to this because latency is zero on the connection.
ctx.num_tickets = 0is a valid workaround.websockets does it in its own test suite:
websockets/tests/utils.py
Lines 30 to 38 in ec074d2
The actual upstream issue is openssl/openssl#7967 and OpenSSL maintainers are not interested.
aaugustin commentedon Oct 14, 2026
This problem doesn't happen on real connections because latency is non-zero.
I don't want to set
num_tickets = 0for everyone; tickets are actually useful.People who hit this in their test suites, like you and me, just put the workaround.
olegrtecno commentedon Oct 15, 2026
Good to know. Thanks!