Hands-on RLIMIT_NOFILE lab: soft=64 → EMFILE after 58 held accepts; soft=512 takes 200/200. Soft raises without root; hard does not. Real numbers.
5 min read
“Too many open files” (EMFILE, errno 24) is the incident that looks like a network outage while CPU is idle. The usual knob people reach for is ulimit -n — and half the time they mix up soft vs hard.
This is a hands-on lab with measured numbers:
RLIMIT_NOFILE mean on a live process./proc/<pid>/limits during an incident.Related links:
Lab honesty (30 Sep 2026 IST): Shared Linux lab box (8 cores). Python 3.13.5 used resource.setrlimit(RLIMIT_NOFILE, …) inside the process only — no host-wide sysctl, no Docker. TCP demo bound to 127.0.0.1 only. Affiliates: 0.
Verdict up front: soft is the ceiling that throws EMFILE. With soft=64 our holding accept server took 58 connections then hit EMFILE; with soft=512 the same 200-client flood was 200/200 with zero EMFILE. Soft raised to 4096 without root; raising hard failed with ValueError: not allowed to raise maximum limit.
| Limit | Role in this lab |
|---|---|
| Soft | Effective ceiling for the process right now |
| Hard | Ceiling you may raise soft up to without privilege |
ulimit -n / -Sn | Soft |
ulimit -Hn | Hard |
/proc/<pid>/limits → Max open files | Soft and hard columns for that PID |
Related links:
Shell baseline on this box before we touched anything: soft = hard = 524288. That is unusually high for a laptop image and common on some container/CI hosts — which is why teams still ship with soft=1024 in systemd units and get surprised in prod.
We counted FDs already open via /proc/self/fd so “opened + pre-existing ≈ soft” was visible.
| Soft | What we opened | Count before EMFILE | Approx FDs used | Error |
|---|---|---|---|---|
| 256 | regular files | 251 | ~257 | errno 24 Too many open files |
| 64 | unbound TCP sockets | 59 | ~65 | errno 24 |
| 1024 | unbound TCP sockets | 1019 | ~1025 | errno 24 |
Pre-existing FDs in the process were 6 (stdin/out/err + a few). Soft is the budget; the kernel does not care that hard is still 524288.
Permission checks we also ran:
| Attempt | Result |
|---|---|
| soft → 4096 (hard unchanged at 524288) | OK — no root |
| soft = hard + 1 | ValueError: current limit exceeds maximum limit |
| hard → hard + 1 | ValueError: not allowed to raise maximum limit |
After raising soft to 4096, /proc/self/limits showed: Max open files 4096 524288.
Same client flood: 200 concurrent connects to 127.0.0.1, server keeps accepted sockets open (classic connection leak / FD leak shape).
| Server soft | Accepted & held | EMFILE on further accept? | Client OK |
|---|---|---|---|
| 64 | 58 | Yes (first EMFILE after 58 accepts) | partial (server stopped taking more) |
| 512 | 200 | No (0 samples) | 200 / 200 |
This is the production story: the listen socket is still up, CPU is fine, and accept() starts returning EMFILE. Clients see stalls, resets, or refusals depending on timing — dashboards scream “network” while ls /proc/<pid>/fd | wc -l is sitting on the soft ceiling.
Keep-alive reduces FD churn by reusing connections. A connection leak increases FD use until soft kills you. Listen backlog absorbs SYN/accept delay — it does not raise RLIMIT_NOFILE. Three different knobs; three different graphs.
Related links:
LimitNOFILE= is what matters for daemons./proc/<pid>/limits inside the workload.nofile deep dive — we measured process rlimits only.cat /proc/<pid>/limits and ls /proc/<pid>/fd | wc -l.close, unbounded connection pools) — a higher ulimit only buys time.Soft RLIMIT_NOFILE is the limit that produces EMFILE. We reproduced it on schedule (soft 256 → 251 files; soft 64 → 59 sockets) and in an accept-hold flood (58 held then EMFILE vs 200/200 at soft 512). Raise soft within hard without root; treat hard raises and leak fixes as separate workstreams.
Evidence path on the lab box: lab-evidence/17-ulimit-fds/results/. Affiliates: 0.
Lab evidence
Lab 30 Sep 2026 IST. Python 3.13.5. Shell baseline soft=hard=524288. setrlimit: soft=256 files opened 251 then errno 24 EMFILE (ceiling≈257); soft=64 sockets opened 59 then EMFILE; soft=1024 sockets opened 1019 then EMFILE. soft>hard → ValueError; raise soft→4096 OK; raise hard → ValueError not allowed. Holding-accept server flood n=200: soft=64 accepted_and_held=58 then EMFILE; soft=512 accepted 200/200 emfile=0. Affiliates: 0. Evidence: lab-evidence/17-ulimit-fds/.
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