#19704·micropython

modlwip: non-blocking socket blocks in send when out of memory

Author: srggCreated Sep 16, 2026Updated Sep 16, 2026

Port, board and/or hardware

Not port-specific — the code is the generic lwip path in extmod/modlwip.c, shared by every MICROPY_PY_LWIP port (rp2, stm32, mimxrt, esp8266, …). Measured on an OpenMV RT1062 (i.MX RT1062, cyw43 WiFi).

MicroPython version

master: extmod/modlwip.c, lwip_tcp_send, the ERR_MEM for (;;) loop (around L1091–1110) that waits with poll_sockets() under a 10 s cap. The same loop is present on the v1.28.0 line, where it waits with mp_hal_delay_ms(50); the numbers below were measured on a v1.28.0-based OpenMV fork.

Reproduction

  1. A non-blocking socket (setblocking(False)) writing to a peer whose receive window backs up, on a port whose MEM_SIZE / MEMP_NUM_TCP_SEG pool is small enough to exhaust under the unacked queue while tcp_sndbuf still reports room.
  2. select.select([], [sock], []) returns the socket writable (POLLOUT reads tcp_sndbuf).
  3. sock.write(buf) then blocks inside lwip_tcp_send instead of returning EAGAIN.

The trigger is tcp_write returning ERR_MEM while tcp_sndbuf is non-zero — the byte counter and the segment/heap pool are different resources and disagree under memory pressure.

Expected behaviour

sock.write() on a non-blocking socket returns EAGAIN, as it already does from the tcp_sndbuf == 0 path in the same function. The caller retries; no data is lost, because tcp_write queues zero bytes on ERR_MEM.

Observed behaviour

sock.write() blocks up to the 10 s cap. The ERR_MEM loop has no socket->timeout == 0 check, so a non-blocking socket waits there. The loop documents this itself:

// Note: if the socket is non-blocking, then this code will actually block until
// there's enough memory to do the write, but by this stage, we have already
// committed to being able to write the data.

The "committed" rationale does not hold for a non-blocking socket: lwip_tcp_send has returned nothing to the caller yet, and the commitment rests on tcp_sndbuf while the failure comes from the segment/heap pool. On a single-threaded event loop, the block stalls every other task. Measured on the OpenMV RT1062 serving an MJPEG stream to a reader throttled to 20 KB/s: the worst single write() reached 1553086 us and 54 events exceeded 100 ms over a 320 s run.

Additional Information

A guard in the ERR_MEM loop returning EAGAIN when socket->timeout == 0 — after tcp_output(), before the wait — cut the worst write() to 3484 us and the over-100 ms events to 0 on the same test, with no change for a blocking socket. Happy to open a PR against master if the direction is agreeable.

Code of Conduct

Yes, I agree.