ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
About

Plate 64

  1. Blog

selectors vs select.select: Localhost Lab

Aditya Challa·1 October 2026·4 min read

Summary
On this page
  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table (p50 ops/s)
  5. Reading it for SRE work
  6. Reuse lesson
  7. Why selectors still wins product-wise
  8. Scaling sketch
  9. Pitfalls
  10. Reproduce
  11. Limits
  12. Takeaway

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

ArmPattern
DefaultSelector n=8/32/128new selector, register all, select(0), close
select.select same none select call on FD list
reuse n=32register once, many selects
empty pollno FDs

Seven rounds, p50. Socketpairs pre-loaded with one ready byte each.


Lab topology

n_iters=2000 · timeout=0 · backend=EpollSelector
metric: ops/s = n_iters / p50_s

Script: lab-evidence/136-selectors-vs-select/results/run_lab.py.


Lead table (p50 ops/s)

Armops/s
select n8916368
DefaultSelector n865635
select n32351008
DefaultSelector n3217977
select n12872034
DefaultSelector n1284244
select reuse n32304282
DefaultSelector reuse n3285037
DefaultSelector empty2824994
select empty2162590

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.select is 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

python3 lab-evidence/136-selectors-vs-select/results/run_lab.py

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.

pythonioepollperformancemultiplexingnetworking

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.

Notes when a lab post goes up

Occasional email for new hands-on reviews. No sequence and no sponsors.

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

On this page

  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table (p50 ops/s)
  5. Reading it for SRE work
  6. Reuse lesson
  7. Why selectors still wins product-wise
  8. Scaling sketch
  9. Pitfalls
  10. Reproduce
  11. Limits
  12. Takeaway
All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove