Chapter 43 — Resource limits: ulimit and your first taste of cgroups
In one sentence
ulimit caps what a SINGLE process (or shell session) can consume — open
files, memory, processes — while cgroups (control groups) let the kernel
enforce resource limits on a whole GROUP of processes together, and cgroups
are the exact mechanism that makes container resource limits (Phase 3)
possible at all.
Why this matters
This chapter is the direct bridge into Phase 3: everything Docker and Kubernetes do to limit a container's CPU/memory is built on cgroups, which are built on ideas you've now already learned (Chapters 8, 10, 13).
How it actually works
ulimit -a # show ALL current limits for this
# shell (and anything it launches)
ulimit -n # just open file descriptors
# (Chapter 23) -- a VERY commonly hit
# limit in real production systems
ulimit -n 4096 # raise it (for THIS shell session;
# permanent changes need config files --
# `/etc/security/limits.conf` on most
# systems)
ulimit -u # max number of processes this user
# can create -- Chapter 31's fork-bomb
# defence, in practice
CGROUPS (control groups) let the kernel group a set of PROCESSES
together and apply RESOURCE LIMITS to the GROUP AS A WHOLE -- CPU
time, memory, I/O bandwidth, process count -- enforced directly by
the kernel, not just requested politely:
/sys/fs/cgroup/... cgroups are exposed as a VIRTUAL FILESYSTEM
(Chapter 23's exact philosophy again) -- you
create a cgroup by making a directory, and
configure it by writing to files inside it.
THIS IS PRECISELY THE MECHANISM CONTAINERS USE (Phase 3, in full):
when you run `docker run --memory=512m ...`, Docker is, under the
hood, creating a cgroup for that container's processes and writing
`512m` into that cgroup's memory-limit file. If the container's
processes collectively try to exceed it, the KERNEL'S OOM killer
(Chapter 13) -- scoped specifically to that cgroup -- kills a process
inside it, exactly as Chapter 13 described, just now applied to a
GROUP of processes instead of the whole machine.
Real-world example
"Too many open files" (often surfaced as the error `EMFILE` at the
syscall level, or a plain "too many open files" message in
application logs) is one of the most common, most WELL-DOCUMENTED
real production incidents caused directly by an unraised `ulimit`:
the default per-process open-file-descriptor limit on many Linux
distributions has historically been a relatively low number (1024 is
a very common historical default) -- entirely reasonable for a
typical desktop process, but FAR too low for a high-connection-count
server process, where EVERY open network connection ALSO consumes a
file descriptor (Chapter 19/23).
This is so well-documented and so common that major server software
-- nginx, Redis, PostgreSQL, and Elasticsearch all explicitly, in
their OWN official production-deployment documentation, instruct
operators to RAISE the open-file-descriptor ulimit before running
them under real load, specifically because the DEFAULT will cause
real, production connection failures once traffic exceeds roughly a
thousand concurrent connections -- a limit that's trivially reachable
for any moderately successful real-world service. This is a genuine,
concrete, and extremely common real-world case of Chapter 4's
abstract "processes aren't free, and neither are the things they
hold open" lesson becoming an actual, documented, easily Googled
production outage.
Try it yourself
# See your current limits
ulimit -a
# See the specific, commonly-hit one
ulimit -n
# Watch a process hit the open-file limit (safe, contained demo)
(
ulimit -n 10 # deliberately, TEMPORARILY set it very low,
# in a SUBSHELL so it doesn't affect your real
# session
python3 -c "
files = []
try:
for i in range(20):
files.append(open('/tmp/testfile_%d' % i, 'w'))
print(f'opened file {i}')
except OSError as e:
print(f'FAILED at file {len(files)}: {e}')
import os
for i in range(len(files)):
os.remove('/tmp/testfile_%d' % i)
"
)
# Explore cgroups on your own system, read-only
ls /sys/fs/cgroup/ 2>/dev/null | head -15
cat /sys/fs/cgroup/memory.max 2>/dev/null || \
echo "(cgroup v2 path may differ, or you may need sudo -- this is
informational only)"
Build it in Go
Chapter 78 hits EMFILE ("too many open files") on
purpose and shows that Go quietly raises its own soft RLIMIT_NOFILE to the
hard limit (minus one) at startup. Chapters 75–76
read cgroup v2 files (cpu.max, cpu.stat, memory.max) from inside a
container.
Common mistakes
- Running a high-connection-count service on default ulimits without checking. A very common, avoidable real production outage — check your server software's OWN documentation for its recommended limits.
- Confusing
ulimit(per-process/per-user) with cgroups (per-group, kernel-enforced, what containers actually use). They're related ideas, solving overlapping but distinct problems, at different granularities. - Setting a
ulimitchange in your current shell and expecting it to persist. It only applies to that shell (and its children) — permanent changes need/etc/security/limits.confor a systemd unit'sLimitNOFILE=directive (Chapter 33).
Check yourself
- What's the difference between
ulimitand a cgroup, in terms of scope? - Why does a high-connection-count server commonly need its open-file ulimit raised?
- How does this chapter's cgroup mechanism directly become Phase 3's container memory limits?
Further reading
- Reference:
man 1 ulimit(as part ofman 1 bash),man 5 limits.conf,man 7 cgroups. - Docs: nginx's, Redis's, or PostgreSQL's own official production deployment/tuning documentation — search any one of them for "ulimit" or "open files" to see the real, documented recommendation.
- Book: Systems Performance (2nd ed.), Gregg — the cgroups material connects directly into container performance analysis.
End of Phase 2 — Milestone check
- I can navigate the Linux filesystem confidently and explain WHY
/etc,/var, and/usrhold what they hold - I'm fluent with
grep/sed/awk/pipes for real text-processing tasks - I understand file permissions, ownership, hard vs. symbolic links, and what SUID actually does
- I can watch and control processes with
ps/top/kill, and manage background jobs that survive a disconnect - I can write and manage a real systemd service, with a sensible restart policy
- I know where logs actually go, and why log rotation is a day-one deployment concern, not an afterthought
- I can write a defensive bash script with
set -euo pipefail, and explain exactly what each flag protects against - I understand the cron environment gap and how to guard against it
- I can correctly read load average,
vmstat, andiostat— and explain why "high load" doesn't always mean "CPU pressure" on Linux - I can use
straceandlsoftogether to diagnose a stuck process or a disk-space mystery - I understand
ulimitand have a first working mental model of cgroups, ready for Phase 3