Managing System Resources with the `ulimit` Command on Linux
The `ulimit` command is a built-in shell utility on Unix and Linux systems that enforces per-process and per-user resource limits, preventing any single process or user from exhausting system resources such as CPU time, memory, open file descriptors, and process count. It operates at the kernel level through the `setrlimit()` system call, making it one of the most direct and low-overhead mechanisms available to system administrators for resource governance.
For any server running production workloads — whether a high-traffic web application, a database engine, or a containerized microservice stack — misconfigured or absent `ulimit` settings are a leading cause of cascading failures, runaway processes, and full system outages. Getting these limits right is not optional; it is foundational infrastructure hygiene.
How `ulimit` Works Under the Hood
When a shell process calls `ulimit`, it invokes the `getrlimit()` and `setrlimit()` system calls defined in the POSIX standard. Each limit is represented as a pair of values: a soft limit and a hard limit. These are stored per-process in the kernel's process descriptor and are inherited by child processes at `fork()` time.
This inheritance model is critical to understand. If you set `ulimit` values in a shell session, every process spawned from that shell — including daemons launched via init scripts — inherits those limits. Conversely, limits set in `/etc/security/limits.conf` apply at PAM login time, not at runtime, which means they only take effect for new login sessions, not for already-running services.
Soft Limits vs. Hard Limits
| Property | Soft Limit | Hard Limit |
|---|---|---|
| — | — | — |
| Who can raise it | Any unprivileged user (up to the hard limit) | Only root (`CAP_SYS_RESOURCE`) |
| Who can lower it | Any user | Any user (irreversible without root) |
| Enforcement | Enforced by the kernel | Acts as the ceiling for the soft limit |
| Typical use case | Day-to-day operational boundary | Absolute maximum for security policy |
| Flag in `ulimit` | `-S` | `-H` |
A common operational mistake is setting the hard limit equal to the soft limit. This removes all flexibility for a process to temporarily raise its own limits, which some applications (like certain JVM implementations and database engines) do legitimately during startup.
Complete Reference: `ulimit` Resource Flags
| Flag | Resource | Unit | Common Production Value |
|---|---|---|---|
| — | — | — | — |
| `-t` | CPU time | Seconds | `unlimited` for daemons |
| `-f` | Maximum file size | 512-byte blocks | `unlimited` or specific cap |
| `-d` | Data segment (heap) size | KB | `unlimited` for Java apps |
| `-s` | Stack size | KB | `8192` (default) |
| `-c` | Core dump file size | 512-byte blocks | `0` (disabled in prod) |
| `-m` | Maximum resident set size | KB | Rarely enforced (use cgroups) |
| `-v` | Virtual memory (address space) | KB | `unlimited` for most services |
| `-n` | Open file descriptors | Count | `65536` or higher for busy servers |
| `-u` | Maximum user processes | Count | `4096`–`65536` depending on role |
| `-l` | Locked memory (mlock) | KB | High for Redis, Elasticsearch |
| `-i` | Pending signals | Count | System default usually sufficient |
| `-q` | POSIX message queue bytes | Bytes | System default |
| `-r` | Real-time scheduling priority | Priority | `0` unless RT workloads |
| `-e` | Maximum scheduling priority (nice) | Nice value | System default |
Practical `ulimit` Usage with Real-World Context
Viewing Current Limits
“`bash
ulimit -a # All soft limits for the current shell
ulimit -aH # All hard limits for the current shell
“`
To inspect limits for a specific running process (PID), read directly from the proc filesystem — this is the authoritative source and bypasses shell-level reporting:
“`bash
cat /proc/<PID>/limits
“`
This is invaluable when troubleshooting a service that was started by systemd or an init script, where shell-level `ulimit -a` will not reflect the process's actual limits.
Setting Soft and Hard Limits
“`bash
Set soft limit for open file descriptors
ulimit -Sn 65536
Set hard limit for open file descriptors
ulimit -Hn 131072
Set both simultaneously (soft = hard = value)
ulimit -n 65536
“`
Disabling Core Dumps in Production
“`bash
ulimit -c 0
“`
Core dumps can consume gigabytes of disk space within seconds when a high-memory process crashes. Disabling them in production is standard practice unless you are actively debugging. For development environments, set a dedicated path using `sysctl kernel.core_pattern` alongside a non-zero core limit.
Restricting CPU Time for Untrusted Processes
“`bash
ulimit -t 30
“`
This sends `SIGXCPU` to the process when it reaches the soft CPU time limit, and `SIGKILL` at the hard limit. This is particularly useful in shared hosting environments or when running user-submitted scripts.
Raising the Open File Descriptor Limit for High-Concurrency Services
Nginx, HAProxy, PostgreSQL, and Redis all require a high number of open file descriptors under load. The system-wide default of 1024 is dangerously low for production:
“`bash
ulimit -n 65536
“`
However, this only affects the current shell session. For persistent configuration, use the methods described in the next section.
Making `ulimit` Settings Persistent
Method 1: `/etc/security/limits.conf`
This is the standard PAM-based approach for user-level persistent limits:
“`
/etc/security/limits.conf
<domain> <type> <item> <value>
- soft nofile 65536
- hard nofile 131072
nginx soft nproc 4096
nginx hard nproc 8192
postgres soft nofile 65536
postgres hard nofile 65536
postgres soft memlock unlimited
postgres hard memlock unlimited
“`
The wildcard `*` applies to all users but does not apply to root. Root requires an explicit entry:
“`
root soft nofile 65536
root hard nofile 131072
“`
Ensure the PAM module is loaded. Verify `/etc/pam.d/common-session` (Debian/Ubuntu) or `/etc/pam.d/system-auth` (RHEL/CentOS) contains:
“`
session required pam_limits.so
“`
Method 2: `/etc/security/limits.d/` Drop-in Files
For cleaner management, especially in configuration management systems like Ansible or Puppet, place service-specific limit files in the drop-in directory:
“`bash
/etc/security/limits.d/99-nginx.conf
nginx soft nofile 65536
nginx hard nofile 131072
“`
Files in this directory are processed after `limits.conf` and override it, making them ideal for application-specific tuning without modifying the base configuration.
Method 3: systemd Service Units (The Modern Standard)
For services managed by systemd — which is the majority of modern Linux distributions — `limits.conf` is not applied by default. systemd manages its own resource limits per service unit:
“`ini
/etc/systemd/system/nginx.service.d/limits.conf
[Service]
LimitNOFILE=65536
LimitNPROC=4096
LimitCORE=0
LimitMEMLOCK=infinity
“`
After editing, reload and restart:
“`bash
systemctl daemon-reload
systemctl restart nginx
“`
Verify the applied limits:
“`bash
cat /proc/$(systemctl show -p MainPID nginx | cut -d= -f2)/limits
“`
This is the most reliable method for production services and should be the default approach on any system running systemd (Ubuntu 16.04+, CentOS 7+, Debian 8+).
Method 4: Shell Profile Files
For user-session limits that apply interactively, add `ulimit` commands to `/etc/profile` (system-wide) or `~/.bashrc` / `~/.profile` (per-user). This approach is appropriate for developer workstations but is unsuitable for daemon processes.
Role-Based `ulimit` Configuration Profiles
Different server roles demand fundamentally different resource limit profiles. Applying generic defaults across all server types is a common source of subtle, hard-to-diagnose failures.
Web Server (Nginx / Apache)
“`
nofile: 65536–131072 # High concurrency requires many open sockets + files
nproc: 4096 # Worker processes + threads
core: 0 # Disable core dumps in production
“`
Relational Database (PostgreSQL / MySQL)
“`
nofile: 65536 # Many concurrent connections = many file descriptors
memlock: unlimited # Required for shared memory and huge pages
nproc: 4096
stack: 8192 KB
core: 0
“`
Java Application Server (Tomcat / Spring Boot)
“`
nofile: 65536
nproc: 65536 # JVM thread-per-connection models spawn many threads
data: unlimited # JVM heap is allocated from the data segment
stack: 512 KB # Reduce stack size to fit more threads in memory
“`
Redis / In-Memory Data Store
“`
nofile: 65536
memlock: unlimited # Prevents swapping of memory-mapped data
“`
Critical Pitfalls and Edge Cases
The `nproc` limit counts threads, not just processes. On Linux, threads are implemented as lightweight processes (`clone()` with shared memory). A Java application with 500 threads counts as 500 against the `nproc` limit. This surprises many administrators who set conservative `nproc` values and then wonder why their JVM crashes with `OutOfMemoryError: unable to create new native thread`.
`ulimit -v` limits virtual address space, not physical RAM. Many administrators set `-v` thinking they are capping memory usage. In reality, they are capping the virtual address space, which includes memory-mapped files, shared libraries, and JVM metaspace. Setting this too low will cause `mmap()` failures and cryptic application errors.
`ulimit` does not apply retroactively. Changing limits in `limits.conf` or a systemd unit file does not affect already-running processes. You must restart the service for new limits to take effect.
Container environments bypass `ulimit` in unexpected ways. In Docker, `ulimit` defaults are set at the daemon level (`/etc/docker/daemon.json`) and can be overridden per container with `–ulimit`. However, the container's limits are bounded by the host kernel's limits. Setting `nofile=1048576` in a container while the host has `nofile=65536` will silently fall back to the host limit.
The `nofile` system-wide ceiling is separate from per-process limits. The kernel parameter `fs.file-max` (set via `sysctl`) controls the total number of file descriptors across the entire system. Even if per-process `nofile` is set high, hitting `fs.file-max` will cause `ENFILE` errors system-wide. Check and tune both:
“`bash
sysctl fs.file-max
sysctl -w fs.file-max=2097152
“`
`ulimit` vs. cgroups: Choosing the Right Tool
| Capability | `ulimit` / `setrlimit` | cgroups v2 |
|---|---|---|
| — | — | — |
| Scope | Per-process (inherited by children) | Per-group of processes |
| Memory limiting | Virtual address space only (`-v`) | Actual RSS + swap enforcement |
| CPU throttling | CPU time budget (`-t`) | CPU bandwidth controller (precise %) |
| I/O limiting | Not supported | Block I/O weight and rate limits |
| Network limiting | Not supported | Requires tc + cgroup integration |
| Persistence | Via PAM or systemd | Via systemd slices or cgroupfs |
| Container compatibility | Limited | Native (Docker, Kubernetes use cgroups) |
| Granularity | Coarse | Fine-grained |
`ulimit` remains the right tool for quick, per-session limits, file descriptor caps, and core dump control. For comprehensive resource isolation — especially in multi-tenant environments or containerized workloads — cgroups v2 is the superior mechanism. On a well-configured VPS Hosting or Dedicated Server environment, both mechanisms are typically used in combination: `ulimit` for per-process guardrails and cgroups for aggregate resource budgets.
Monitoring and Validating Resource Limits
Proactive monitoring prevents limit-related failures from becoming production incidents.
Check current file descriptor usage system-wide:
“`bash
cat /proc/sys/fs/file-nr
Output: <allocated> <unused> <max>
“`
Find processes approaching their `nofile` limit:
“`bash
for pid in /proc/[0-9]*; do
pid_num=${pid##*/}
limit=$(awk '/Max open files/{print $4}' /proc/$pid_num/limits 2>/dev/null)
current=$(ls /proc/$pid_num/fd 2>/dev/null | wc -l)
[ -n "$limit" ] && [ "$limit" != "unlimited" ] &&
awk -v c=$current -v l=$limit -v p=$pid_num
'BEGIN{if(c/l>0.8) printf "PID %s: %d/%d (%.0f%%)n",p,c,l,c/l*100}'
done
“`
Tools for ongoing monitoring:
- `lsof -u <username>` — list all open files for a user
- `ss -s` — socket statistics (correlates with `nofile` pressure)
- `htop` with process tree view — visualize process counts per user
- `sar -v` — historical file descriptor and inode usage via sysstat
- Prometheus `node_exporter` — exposes `node_filefd_allocated` and `node_filefd_maximum` metrics for alerting
For environments running VPS with cPanel or other control panels, many of these limits are pre-configured by the panel installer, but they frequently need upward adjustment as traffic grows. Always verify the actual limits against `/proc/<PID>/limits` rather than trusting panel documentation.
Security Implications of `ulimit`
Resource limits are also a security control. Without them, a compromised or buggy process can execute a fork bomb (`:(){ :|:& };:`), exhausting all available process slots and rendering the system unresponsive. A conservative `nproc` limit per user is the primary mitigation:
“`
- hard nproc 4096
“`
Similarly, disabling core dumps (`-c 0`) prevents sensitive memory contents — including encryption keys, passwords, and session tokens — from being written to disk in a world-readable file.
For shared hosting environments or any server where multiple users have shell access, `ulimit` is a mandatory security layer. On Shared Web Hosting infrastructure, these limits are typically enforced at the platform level, but administrators running their own multi-user VPS should configure them explicitly.
If your server handles SSL termination or certificate management, ensure the process handling TLS (e.g., Nginx, HAProxy) has sufficient `nofile` limits, as each TLS connection requires multiple file descriptors. Pair this with properly configured SSL Certificates to avoid certificate-related connection failures compounding resource issues.
For mail server deployments, Postfix and Dovecot are particularly sensitive to `nofile` limits, as each concurrent email connection and mailbox access consumes file descriptors. If you are running your own mail infrastructure rather than using managed Email Hosting, tuning `nofile` to at least 65536 for the mail user is non-negotiable on any moderately loaded server.
Decision Matrix: What to Configure and Where
| Scenario | Recommended Method | Key Parameters |
|---|---|---|
| — | — | — |
| Interactive user sessions | `/etc/security/limits.conf` | `nofile`, `nproc`, `core` |
| systemd-managed service | systemd unit `[Service]` section | `LimitNOFILE`, `LimitNPROC`, `LimitCORE` |
| Docker container | `–ulimit` flag or `daemon.json` | `nofile`, `nproc` |
| One-time shell testing | `ulimit` command directly | Any flag |
| Multi-tenant shared server | `limits.conf` + PAM enforcement | `nproc`, `nofile`, `fsize`, `cpu` |
| Kubernetes pod | Pod security context + cgroups | Managed by kubelet |
| Application-specific tuning | `limits.d/` drop-in file | Service-specific parameters |
Technical Key-Takeaway Checklist
- Always verify applied limits via `/proc/<PID>/limits`, not shell-level `ulimit -a`, for running services.
- For systemd services, configure limits in the unit file using `Limit*` directives — `limits.conf` is not read by systemd by default.
- Set `nofile` to at minimum `65536` for any service handling network connections; `131072` or higher for high-concurrency workloads.
- Never set the hard limit equal to the soft limit unless you have a specific security requirement — applications need headroom to self-adjust.
- Disable core dumps (`LimitCORE=0`) in production; enable them with a controlled path in staging.
- The `nproc` limit counts threads on Linux — account for this when configuring JVM or Go runtime applications.
- Tune `fs.file-max` via `sysctl` alongside per-process `nofile` limits to avoid system-wide `ENFILE` exhaustion.
- In containerized environments, host kernel limits are the hard ceiling — container-level `ulimit` settings cannot exceed them.
- Use cgroups v2 for memory and I/O enforcement; use `ulimit` for file descriptor caps, process counts, and core dump control.
- After any limit change in `limits.conf` or systemd unit files, restart the affected service and verify with `/proc/<PID>/limits`.
FAQ
Does `ulimit` apply to root processes?
The wildcard `*` in `/etc/security/limits.conf` explicitly excludes root. Root processes also bypass hard limit enforcement for most resource types — root can raise its own hard limits. To apply limits to root, add an explicit `root` entry in `limits.conf`, though many system services running as root will ignore PAM-applied limits if started outside a login session.
Why does my `limits.conf` change have no effect on a running service?
`limits.conf` is applied by PAM at login time. Services started by systemd, SysVinit, or Upstart do not go through PAM and therefore do not inherit `limits.conf` settings. Configure limits directly in the systemd unit file using `LimitNOFILE` and related directives, then run `systemctl daemon-reload && systemctl restart <service>`.
What is the maximum value I can set for `nofile`?
The per-process maximum is bounded by the kernel's `fs.nr_open` parameter (default: 1,048,576 on most kernels). The system-wide total is bounded by `fs.file-max`. You can raise `fs.nr_open` via `sysctl`, but values above 1,048,576 require kernel recompilation on older kernels. Practically, 524,288 or 1,048,576 covers virtually all production use cases.
How do I check if a process has hit its `ulimit` boundary?
Check the kernel log with `dmesg | grep -i "ulimit|RLIMIT|too many open|cannot allocate"`. Application logs will typically show `EMFILE` (too many open files), `ENOMEM` (memory allocation failure), or `EAGAIN` (resource temporarily unavailable). Cross-reference with `/proc/<PID>/limits` and current descriptor count via `ls /proc/<PID>/fd | wc -l`.
Is `ulimit` sufficient for resource isolation in a multi-tenant environment?
No. `ulimit` provides per-process and per-user guardrails but does not enforce memory bandwidth, disk I/O, or network throughput limits. For true multi-tenant isolation, combine `ulimit` with cgroups v2 resource controllers, and consider namespace isolation (user namespaces, PID namespaces) for stronger security boundaries. On managed infrastructure, these controls are typically layered at the hypervisor and container runtime level.
