Linux caps how many files, processes and other resources each process may use. The defaults are safe for desktops but too low for busy web servers, databases and proxies, which then fail with errors such as Too many open files or fork: Resource temporarily unavailable. In this tutorial you will inspect the current limits on Ubuntu 24.04, raise them for login users with /etc/security/limits.d/, raise them for systemd services with LimitNOFILE, and confirm that the running processes actually picked up the new values.
The key point to keep in mind: limits are applied per process by whatever started it. Users logging in over SSH get limits from PAM (limits.conf), while services started by systemd ignore limits.conf entirely and take their limits from the unit file. Most "I changed limits.conf and nothing happened" problems come from mixing these two up.
Prerequisites
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
- A non-root user with
sudoprivileges. - Optionally, a service that needs higher limits. This guide uses Nginx as the example (
sudo apt install nginx).
Step 1 - Understanding soft and hard limits
Every limit has two values:
- Soft limit: the value the kernel enforces right now. A process can raise it itself, up to the hard limit.
- Hard limit: the ceiling for the soft limit. Only root (or a process with
CAP_SYS_RESOURCE) can raise it.
The limits you will change most often are:
Item (limits.conf) | systemd directive | ulimit flag | Controls | Typical error when too low |
|---|---|---|---|---|
nofile | LimitNOFILE | -n | Open file descriptors (files, sockets, pipes) | Too many open files |
nproc | LimitNPROC | -u | Processes and threads per user | fork: Resource temporarily unavailable |
memlock | LimitMEMLOCK | -l | Memory that can be locked in RAM | Database or VPN fails to lock memory |
core | LimitCORE | -c | Size of core dumps | No core dump written after a crash |
stack | LimitSTACK | -s | Stack size per thread | Stack overflow in deep recursion |
For most servers, nofile is the one that matters. Every TCP connection, log file and upstream socket counts against it.
Step 2 - Checking the current limits
Display the soft limits of your current shell:
ulimit -a
real-time non-blocking time (microseconds, -R) unlimited
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 15395
max locked memory (kbytes, -l) 500384
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 15395
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
Check the soft and hard values for open files specifically:
ulimit -Sn
ulimit -Hn
1024
524288
The numbers for pending signals, locked memory and processes depend on the amount of RAM, so yours will differ. The soft nofile limit of 1024 is the one that usually causes trouble.
The most reliable way to see the limits of a process that is already running is /proc/<PID>/limits. For example, for the oldest Nginx process (the master):
cat /proc/$(pgrep -o nginx)/limits
Limit Soft Limit Hard Limit Units
Max cpu time unlimited unlimited seconds
Max file size unlimited unlimited bytes
Max data size unlimited unlimited bytes
Max stack size 8388608 unlimited bytes
Max core file size 0 unlimited bytes
Max resident set unlimited unlimited bytes
Max processes 15395 15395 processes
Max open files 1024 524288 files
...
Always verify against /proc/<PID>/limits after a change. What your shell reports with ulimit says nothing about a daemon started by systemd.
Step 3 - Finding out whether a limit is actually the problem
Before raising anything, confirm that a process is really hitting its limit. Count the open file descriptors of a process and compare the number with its Max open files value:
sudo ls /proc/$(pgrep -o nginx)/fd | wc -l
12
To see which processes hold the most descriptors on the whole system, loop over /proc (this needs root to read other users' processes):
sudo sh -c 'for p in /proc/[0-9]*; do n=$(ls "$p/fd" 2>/dev/null | wc -l); echo "$n $(cat "$p/comm" 2>/dev/null) ${p#/proc/}"; done' | sort -rn | head -5
187 mysqld 1123
64 systemd 1
41 nginx 2310
35 systemd-journal 312
22 snapd 890
Search the logs of a service for limit errors:
sudo journalctl -u nginx --since "1 hour ago" | grep -iE "too many open files|resource temporarily unavailable"
Nginx also writes accept4() failed (24: Too many open files) to /var/log/nginx/error.log when workers run out of descriptors. Error number 24 is EMFILE, the per-process limit.
The system-wide counterpart is fs.file-max. Check it and the current usage:
cat /proc/sys/fs/file-nr
1824 0 9223372036854775807
The three columns are allocated handles, unused handles and the maximum. On Ubuntu 24.04 systemd raises fs.file-max to the largest possible value at boot, so the system-wide limit is almost never the bottleneck and you do not need to set it in sysctl. Focus on the per-process limits instead.
Step 4 - Raising a limit temporarily with ulimit
ulimit changes the limits of the current shell and every program started from it. It is useful for testing, and it is lost when you close the session.
Raise the soft open-files limit up to the hard limit:
ulimit -n 65536
ulimit -n
65536
A normal user cannot go above the hard limit. Trying it produces an error:
ulimit -n 2000000
-bash: ulimit: open files: cannot modify limit: Operation not permitted
To change the limits of a process that is already running without restarting it, use prlimit from util-linux. This is handy as an emergency fix, but it does not survive a restart:
sudo prlimit --pid $(pgrep -o nginx) --nofile=65536:65536
sudo prlimit --pid $(pgrep -o nginx) --nofile
RESOURCE DESCRIPTION SOFT HARD UNITS
NOFILE max number of open files 65536 65536 files
Note that this only changes the master process. Worker processes that already exist keep their old limits.
Step 5 - Setting persistent limits for users with limits.d
Limits for interactive logins (SSH, su -, sudo -i, cron jobs) are applied by the pam_limits module. On Ubuntu 24.04 it is already enabled; confirm it:
grep pam_limits /etc/pam.d/common-session /etc/pam.d/sshd
/etc/pam.d/common-session:session required pam_limits.so
Instead of editing the main /etc/security/limits.conf, create a dedicated file in /etc/security/limits.d/. Files there are read in alphabetical order and survive package upgrades cleanly:
sudo nano /etc/security/limits.d/90-nofile.conf
Add the following lines, replacing your_user with the account that runs your application:
# <domain> <type> <item> <value>
your_user soft nofile 65536
your_user hard nofile 65536
@developers soft nofile 32768
@developers hard nofile 32768
* soft nproc 8192
* hard nproc 16384
The columns are:
- domain: a user name, a group prefixed with
@, or*for all users. The wildcard does not apply toroot; add explicitrootlines if root needs different limits. - type:
soft,hard, or-to set both at once. - item: the resource, such as
nofile,nproc,memlockorcore. - value: a number, or
unlimited(fornofilealways use a number, because the kernel caps it atfs.nr_open).
The new limits apply only to new sessions. Log out and open a new SSH session as your_user, then check:
ulimit -Sn; ulimit -Hn
65536
65536
You can also test without logging out by starting a fresh login shell for that user:
sudo -i -u your_user bash -c 'ulimit -n'
65536
Step 6 - Setting limits for systemd services
Services such as Nginx, MySQL, PostgreSQL, Redis or your own application are started by systemd, not by a PAM login, so limits.conf has no effect on them. Set their limits with a drop-in override for the unit.
First, check what the service currently gets:
systemctl show nginx -p LimitNOFILE -p LimitNOFILESoft -p LimitNPROC
LimitNOFILE=524288
LimitNOFILESoft=1024
LimitNPROC=15395
LimitNOFILE is the hard limit and LimitNOFILESoft is the soft limit. By default systemd gives services a soft limit of 1024 and a hard limit of 524288.
Create an override with systemctl edit, which opens an editor on /etc/systemd/system/nginx.service.d/override.conf:
sudo systemctl edit nginx
Add these lines between the comment markers the editor shows:
[Service]
LimitNOFILE=65536
A single value sets both the soft and hard limit. Use LimitNOFILE=65536:131072 if you want different soft and hard values. Save and exit; systemctl edit reloads the systemd configuration for you. Restart the service so that new processes are started with the new limits:
sudo systemctl restart nginx
Verify both the unit setting and the real process:
systemctl show nginx -p LimitNOFILE -p LimitNOFILESoft
grep "Max open files" /proc/$(pgrep -o nginx)/limits
LimitNOFILE=65536
LimitNOFILESoft=65536
Max open files 65536 65536 files
Some applications have their own setting on top of the OS limit. Nginx workers, for example, inherit the master's limit, but you can set it explicitly in the main context of /etc/nginx/nginx.conf and size worker_connections to match:
worker_rlimit_nofile 65536;
events {
worker_connections 16384;
}
Each proxied request uses two descriptors (client and upstream), so keep worker_connections well below worker_rlimit_nofile. Test and reload Nginx:
sudo nginx -t && sudo systemctl reload nginx
The same systemctl edit approach works for any unit. Other useful directives are LimitNPROC, LimitMEMLOCK (use infinity for no limit) and LimitCORE. If a service is also constrained by TasksMax (visible in systemctl show your_service -p TasksMax), raise that in the same override, because it limits the number of threads independently of LimitNPROC.
NoteTo change the default for every service at once, you can set
DefaultLimitNOFILE=in a drop-in under/etc/systemd/system.conf.d/. Per-service overrides are easier to reason about and are the recommended approach.
Step 7 - Setting limits for Docker containers
Containers get their limits from the Docker daemon, not from the host shell or limits.conf. Set a limit for a single container with --ulimit:
docker run --rm --ulimit nofile=65536:65536 ubuntu:24.04 bash -c 'ulimit -n'
65536
To change the default for all new containers, add default-ulimits to /etc/docker/daemon.json:
sudo nano /etc/docker/daemon.json
{
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Soft": 65536,
"Hard": 65536
}
}
}
If the file already has other settings, merge the default-ulimits key into the existing JSON object. Restart Docker and recreate the containers so they pick up the new default:
sudo systemctl restart docker
In Docker Compose, set it per service with the ulimits key:
services:
app:
image: your_image
ulimits:
nofile:
soft: 65536
hard: 65536
Troubleshooting
The limit changed in limits.conf but the service still shows 1024. The service is started by systemd, which ignores limits.conf. Use sudo systemctl edit your_service as shown in Step 6, restart it, and check /proc/<PID>/limits.
ulimit -n still shows the old value after editing limits.d. Existing sessions keep their limits. Log out completely and open a new SSH session. If you use a terminal multiplexer such as tmux or screen, its server process started before the change; kill it and start a new one.
ulimit: open files: cannot modify limit: Operation not permitted. You are trying to raise the soft limit above the hard limit. Raise the hard limit in limits.d (as root) and start a new session.
The limit is raised but the application still fails. Check whether the application has its own cap (for example worker_rlimit_nofile in Nginx, max_connections in MySQL, maxclients in Redis) and whether the service hits TasksMax instead of LimitNPROC.
Conclusion
You checked the limits of your shell and of running processes, raised them for login users through /etc/security/limits.d/, for systemd services through LimitNOFILE overrides, and for Docker containers through --ulimit and default-ulimits. The rule to remember is to set the limit where the process is started and verify it in /proc/<PID>/limits.
As next steps, tune the application settings that sit on top of these limits (such as Nginx worker_connections or database connection limits), review kernel network settings like net.core.somaxconn for high connection rates, and add file descriptor usage to your monitoring so you notice a leak before it reaches the limit.
