Frontend Labs
⋯

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 ulimit change in your current shell and expecting it to persist. It only applies to that shell (and its children) — permanent changes need /etc/security/limits.conf or a systemd unit's LimitNOFILE= directive (Chapter 33).

Check yourself

  1. What's the difference between ulimit and a cgroup, in terms of scope?
  2. Why does a high-connection-count server commonly need its open-file ulimit raised?
  3. How does this chapter's cgroup mechanism directly become Phase 3's container memory limits?

Further reading

  • Reference: man 1 ulimit (as part of man 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 /usr hold 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, and iostat — and explain why "high load" doesn't always mean "CPU pressure" on Linux
  • I can use strace and lsof together to diagnose a stuck process or a disk-space mystery
  • I understand ulimit and have a first working mental model of cgroups, ready for Phase 3

Generated from real-life-os-guide.md · 8 Oct 2026 16:36