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 sudo privileges.
  • 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 directiveulimit flagControlsTypical error when too low
nofileLimitNOFILE-nOpen file descriptors (files, sockets, pipes)Too many open files
nprocLimitNPROC-uProcesses and threads per userfork: Resource temporarily unavailable
memlockLimitMEMLOCK-lMemory that can be locked in RAMDatabase or VPN fails to lock memory
coreLimitCORE-cSize of core dumpsNo core dump written after a crash
stackLimitSTACK-sStack size per threadStack 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 to root; add explicit root lines if root needs different limits.
  • type: soft, hard, or - to set both at once.
  • item: the resource, such as nofile, nproc, memlock or core.
  • value: a number, or unlimited (for nofile always use a number, because the kernel caps it at fs.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.

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.