Plate 64
selectors vs select.select: Localhost Lab
Aditya Challa4 min read
Intro — what this post promises
I/O readiness with selectors.DefaultSelector vs raw select.select. This lab reports ops/s on Linux localhost for register-and-wait patterns, reuse, and empty polls. Backend here: EpollSelector.
Related links:
- dataclass asdict vs vars localhost lab
- tracemalloc snapshot localhost lab
- sqlite3 vs shelve localhost lab
- gc collect cost localhost lab
- path read text vs open localhost lab
- intenum vs int localhost lab
- cmath vs math hypot localhost lab
- signal vs event wakeup localhost lab
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Affiliates: 0. Differentiates from epoll-vs-select (lab 23) — that post scaled raw epoll/select latency with FD count; this one contrasts the selectors API (create/register/close each iter vs reuse) against select.select.
Verdict up front (n_iters=2000): create+register+wait at 32 FDs — DefaultSelector ~17977 ops/s vs select ~351008. Reuse selector: ~85037 vs select ~304282.
Arms
| Arm | Pattern |
|---|---|
| DefaultSelector n=8/32/128 | new selector, register all, select(0), close |
| select.select same n | one select call on FD list |
| reuse n=32 | register once, many selects |
| empty poll | no FDs |
Seven rounds, p50. Socketpairs pre-loaded with one ready byte each.
Lab topology
Script: lab-evidence/136-selectors-vs-select/results/run_lab.py.
Lead table (p50 ops/s)
| Arm | ops/s |
|---|---|
| select n8 | 916368 |
| DefaultSelector n8 | 65635 |
| select n32 | 351008 |
| DefaultSelector n32 | 17977 |
| select n128 | 72034 |
| DefaultSelector n128 | 4244 |
| select reuse n32 | 304282 |
| DefaultSelector reuse n32 | 85037 |
| DefaultSelector empty | 2824994 |
| select empty | 2162590 |
Recreating a selector every wait dominates. Reuse closed most of the gap but select still led the wait loop.
Reading it for SRE work
- Long-lived servers → one DefaultSelector, register/unregister around accept — do not rebuild per request.
- Tiny scripts with a few FDs → raw
select.selectis fine and simpler. - Portability / registering file objects → selectors is the stdlib facade (EpollSelector on this Linux box).
- Lab 23 still owns FD_SETSIZE cliffs; this post is API overhead.
Reuse lesson
At 32 FDs, create+register+close sat near ~17977 ops/s; reuse jumped to ~85037 — about 4.7× better. Empty polls were fast for both (~2824994 / ~2162590).
Why selectors still wins product-wise
You get a uniform register API, edge/level notes via the backend, and fewer platform #ifdefs than hand-rolled epoll. Pay the abstraction once per process lifetime, not once per wait.
Scaling sketch
At 8 FDs, recreate+register cost already dragged DefaultSelector to ~65635 ops/s against select’s ~916368. At 128 FDs the recreate path fell to ~4244 while select held ~72034. That is not “selectors are slow forever” — it is “do not allocate a new multiplexer per tick.” Long-lived processes should look like the reuse arm.
Empty polls stayed in the millions of ops/s for both APIs, so idle timeout-0 checks are cheap once the selector exists.
Pitfalls
- Building a new DefaultSelector inside a hot accept loop.
- Mixing this microbench with lab 23’s ready-wait µs tables.
- Forgetting to unregister closed FDs.
- Using select.select above FD_SETSIZE (lab 23 cliff).
Reproduce
Evidence: summary.json, summary.txt.
Limits
One Linux box, non-blocking socketpairs, timeout=0. Not asyncio, not thousands of idle FDs.
Takeaway
Reuse DefaultSelector (~85037 ops/s at 32 FDs) instead of recreate (~17977). Raw select.select stays faster per wait (~304282) but selectors is the maintainable default for portable servers.
Lab evidence
What I found running this
Ran the selectors-vs-select lab on Linux localhost today with Python 3.13.5, seven rounds and p50 ops/s. At 32 FDs, recreate/register/close measured DefaultSelector ~17977 ops/s versus select ~351008; reusing the selector reached ~85037. Backend was EpollSelector; empty polls stayed in the millions. Affiliates: 0.
Related links
Plate 90
Path.read_text vs open().read: Localhost Lab
1 Oct 2026
Plate 35
ipaddress vs String Prefix Allowlist: Localhost Lab
Hands-on ipaddress CIDR containment vs naive string prefix allowlists: real checks/s, measured on Linux localhost today in this hands-on lab for SREs.
1 Oct 2026
Plate 57
memoryview vs bytes Slice: Localhost Lab
1 Oct 2026