Repository navigation
asyncio does not set TCP_NODELAY on macOS #148783
Description
Bug report
Bug description:
On macOS, asyncio server silently skips setting TCP_NODELAY on accepted connections.
Reproduce
import asyncio
import socket
async def main():
async def on_connect(reader, writer):
sock = writer.transport.get_extra_info('socket')
nodelay = sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY)
print(f'server side: sock.proto={sock.proto} TCP_NODELAY={nodelay}')
writer.close()
await writer.wait_closed()
srv.close()
srv = await asyncio.start_server(on_connect, '127.0.0.1', 0)
port = srv.sockets[0].getsockname()[1]
reader, writer = await asyncio.open_connection('127.0.0.1', port)
sock = writer.transport.get_extra_info('socket')
nodelay = sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY)
print(f'client side: sock.proto={sock.proto} TCP_NODELAY={nodelay}')
await asyncio.sleep(0.1)
writer.close()
await writer.wait_closed()
await srv.wait_closed()
asyncio.run(main())| Platform | Side | sock.proto |
TCP_NODELAY |
|---|---|---|---|
| Linux | client | 6 ✅ | 1 ✅ |
| Linux | server | 6 ✅ | 1 ✅ |
| macOS | client | 6 ✅ | 4 ✅ |
| macOS | server | 0 ❌ | 0 ❌ |
Cause
In socket.__init__, CPython tries to detect proto with getsockopt + SO_PROTOCOL and falls back to 0 if SO_PROTOCOL is not supported, which is the case on macOS.
cpython/Modules/socketmodule.c
Lines 5749 to 5764 in 8276778
| #ifdef SO_PROTOCOL | |
| if (proto == -1) { | |
| int tmp; | |
| socklen_t slen = sizeof(tmp); | |
| if (getsockopt(fd, SOL_SOCKET, SO_PROTOCOL, | |
| (void *)&tmp, &slen) == 0) | |
| { | |
| proto = tmp; | |
| } else { | |
| set_error(); | |
| return -1; | |
| } | |
| } | |
| #else | |
| proto = 0; | |
| #endif |
While socket.accept does pass in the proto from listening socket, it is silently overridden to zero by the above logic.
Line 300 in 8276778
| sock = socket(self.family, self.type, self.proto, fileno=fd) |
asyncio sets TCP_NODELAY only when sock.proto == socket.IPPROTO_TCP, but it is 0 on macOS for the above reason.
cpython/Lib/asyncio/base_events.py
Lines 193 to 201 in 8276778
| if hasattr(socket, 'TCP_NODELAY'): | |
| def _set_nodelay(sock): | |
| if (sock.family in {socket.AF_INET, socket.AF_INET6} and | |
| sock.type == socket.SOCK_STREAM and | |
| sock.proto == socket.IPPROTO_TCP): | |
| sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) | |
| else: | |
| def _set_nodelay(sock): | |
| pass |
Client socket works fine because no fd is passed to socket.__init__ so the proto detection doesn't happen.
CPython versions tested on:
3.13
Operating systems tested on:
macOS, Linux
Linked PRs
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsTodo
Activity
pythongh-148783: Respect explicit socket proto
pythongh-148783: Respect caller-specified socket proto
tzipora-rose commentedon Oct 20, 2026
Windows is affected too, through two paths, and gh-148784 covers one of them.
The reproduction above on Windows 10, CPython 3.12.10:
sock.protoTCP_NODELAYProactorEventLoop(the default)ProactorEventLoop(the default)WindowsSelectorEventLoopWindowsSelectorEventLoopThe selector loop has the cause described here:
SO_PROTOCOLis not defined on Windows, sosocket.__init__replaces theprotothatsocket.accept()passes with 0 (on 3.12.10,socket.socket(AF_INET, SOCK_STREAM, 6, fileno=fd).protois 0). gh-148784 keeps aprotopassed explicitly, which covers this path.The proactor loop has a second cause, which gh-148784 does not reach:
IocpProactor._get_accept_socketcreates the socket thatAcceptExaccepts into withsocket.socket(family), so itsprotois 0 and_set_nodelayskips it. No file descriptor is passed there, so the detection is not involved. With_get_accept_socketcreating that socket withsocket.SOCK_STREAM, socket.IPPROTO_TCP, the reproduction's server side givessock.proto=6 TCP_NODELAY=1. The proactor transport has called_set_nodelaysince bpo-35380 (gh-79561), but for connections the proactor accepts the call does nothing.Both causes are in main as of 2026-10-20.
Investigated and written in collaboration with Claudia in Claude Code.