Hands-on epoll vs select toy: ready-wait flat ~3.5 µs (8→900 FDs); select scales 4.6→16.9 µs then hits fd≥1024 cliff. Python numbers, no Docker.
5 min read
“Just use epoll” is easy advice. Seeing why select falls over — both the O(n) ready-wait cost and the hard FD_SETSIZE / fd≥1024 cliff — sticks better with numbers.
This is a hands-on toy lab with measured numbers:
epoll / poll / select via Python selectors).SelectSelector raises filedescriptor out of range.Related links:
Lab honesty (30 Sep 2026 IST): Shared Linux lab box (8 cores, kernel 6.12). Python 3.13.5. selectors.DefaultSelector → EpollSelector. Localhost TCP socket pairs only. RLIMIT_NOFILE soft raised to 524288 for the ceiling arms. Toy — not nginx, libuv, or a production event loop. No Docker. Affiliates: 0.
Verdict up front: epoll ready-wait stayed flat at ~3.5 µs from 8→900 FDs. select grew 4.6 → 16.9 µs (8→256) then hit the fd≥1024 cliff. poll scaled without that cliff (3.6 → 32.5 µs).
| Backend | Python class | Notes |
|---|---|---|
| epoll | EpollSelector | O(ready) wakeup; no FD_SETSIZE bitmap |
| poll | PollSelector | Scales with interest set; no fd-number 1024 cliff here |
| select | SelectSelector | Scans interest set; fails if any fd number ≥ 1024 |
Related links:
| Watched FDs | epoll | poll | select |
|---|---|---|---|
| 8 | 3.4 | 3.6 | 4.6 |
| 64 | 3.5 | 5.3 | 7.2 |
| 256 | 3.5 | 10.8 | 16.9 |
| 512 | 3.5 | 17.4 | (skipped — ceiling) |
| 900 | 3.5 | 32.5 | (skipped) |
epoll’s line is the lesson: adding idle FDs did not slow the wakeup in this toy. select and poll paid for a larger interest set even when only one socket was hot.
| Pairs (watched servers) | Max server fd | select() |
|---|---|---|
| 500 | 1005 | OK |
| 512 | 1029 | ValueError: filedescriptor out of range |
| Forced watch fds ≥1103 | ≥1103 | same ValueError (register may succeed; select fails) |
This is the classic fd number ≥ 1024 failure mode, not “you watched too many interesting sockets” in the abstract. Raising ulimit -n does not raise FD_SETSIZE for select. Epoll registered 4096 watched server FDs successfully on the same box.
Related links:
Connection accept queues and multi-listen designs are separate labs (backlog, SO_REUSEPORT).
Related links:
select, keep all watched fd numbers < 1024 — or stop using select.nofile without epoll still leaves the select cliff.epoll ready-wait stayed ~3.5 µs from 8→900 FDs. select slowed 4.6 → 16.9 µs by 256 FDs, then died when a watched fd reached ≥1024. poll kept working but scaled to 32.5 µs at 900 FDs. Raise ulimit for capacity; switch off select for correctness past the bitmap.
Evidence path on the lab box: lab-evidence/23-epoll-vs-select/results/. Affiliates: 0.
Lab evidence
Lab 30 Sep 2026 IST. Python 3.13.5 selectors (DefaultSelector=EpollSelector). Ready-wait p50: epoll flat 3.4–3.5 us from 8→900 watched FDs; poll 3.6→32.5 us; select 4.6→7.2→16.9 us (8/64/256) then SKIP. Select ceiling: n=500 pairs fd max 1005 OK; n=512 fd max 1029 → ValueError filedescriptor out of range; forced fd≥1100 fails select(). Epoll registered 4096 watched FDs OK. Affiliates: 0. Evidence: lab-evidence/23-epoll-vs-select/.
Hands-on itertools.islice vs list slice window lab: real ops/s taking ranges from sequences, measured on Linux localhost in this hands-on lab for SREs.
Lab1 Oct 20264 min read
Hands-on heapq.merge vs sorted(chain) multi-way merge: real records/s on pre-sorted lists, measured on Linux localhost today in this hands-on lab for SREs.
Lab1 Oct 20264 min read
Hands-on mmap MAP_SHARED write+msync vs pwrite region update: real MB/s with durability labels, measured on Linux localhost in this hands-on lab for SREs.
Lab1 Oct 20264 min read