Setting User-Level Boundaries
Think of ulimit like a library card with borrowing limits. Each user gets a card, and the card says: you may borrow at most 50 books at once, and each book may be no thicker than 500 pages. The library (the kernel) enforces these limits automatically — you simply cannot check out book number 51.
ulimit is a shell built-in command that sets resource boundaries on the current shell session and every process that shell spawns. It operates at the level of a single user's session, not across the whole system. There are two tiers of limits:
- Soft limit: The current enforced limit. A process can raise it up to the hard limit without special privileges.
- Hard limit: The ceiling. Only the root user can raise it above the system-configured maximum.
The most important limits in practice:
| Flag | Resource | Default | Why It Matters |
|---|---|---|---|
-n |
Open file descriptors | 1024 | Every TCP socket, open file, and pipe consumes one descriptor. High-traffic servers need tens of thousands. |
-u |
Max user processes | varies | Prevents fork bombs — runaway scripts that spawn processes exponentially until the system freezes. |
-s |
Stack size (KB) | 8192 | Caps memory consumed by infinite recursion before it kills the whole system. |
-v |
Virtual memory (KB) | unlimited | Useful in constrained environments to prevent a single process from reserving enormous address space. |
-c |
Core dump size | 0 | Controls whether crashed processes write a core dump file for debugging. |
Viewing and setting limits: ulimit -a lists every limit for the current session; ulimit -n on its own reports the open-file-descriptor limit, and ulimit -n <value> raises it — though the soft limit can only be raised as high as whatever hard limit is already in place.
Making limits persistent requires editing /etc/security/limits.conf, which uses a <domain> <type> <item> <value> format — for example, granting a specific user a higher soft and hard nofile limit, or capping nproc for everyone by default. Changes here apply to all new login sessions for the specified user or group.
For services managed by systemd, limits.conf is bypassed entirely. Limits must be set directly in the unit file instead, via directives like LimitNOFILE= and LimitNPROC= in the [Service] section.
Interviewers frequently ask: "A service is failing with 'Too many open files' but the server has plenty of RAM and CPU. What do you check?" The answer is ulimit -n for the running process (check /proc/<pid>/limits to see the actual limits of a running process, not just the shell default), then verify /etc/security/limits.conf and the systemd unit file's LimitNOFILE. A common trap: changing limits.conf has no effect on systemd-managed services — you must set LimitNOFILE in the unit file and run systemctl daemon-reload && systemctl restart <service>.
Senior depth — the file descriptor table: Each process has its own file descriptor table, but the underlying open file descriptions are shared kernel objects. When a process forks, the child inherits copies of the parent's file descriptors pointing to the same underlying descriptions. This means a parent process that opens 500 connections and forks 10 workers has effectively consumed 500 descriptors in the parent and 500 in each child — 5,500 total against the limit. Pre-fork server models (like Apache's prefork MPM) are particularly sensitive to this, which is why their ulimit -n requirements are much higher than the connection count alone would suggest.