Most services on a VPS have a hard ceiling that has nothing to do with CPU, RAM, or bandwidth: the number of open file descriptors and processes they are permitted. A socket is a file descriptor, so every connection your web server holds is a descriptor. When the limit is reached, the failure is abrupt and the error messages are misleading — accept4() failed (24: Too many open files) in Nginx, or an application that simply stops responding. Here is how the limits stack and how to raise them once, correctly.
Four layers, all of which must agree
This is the reason limit changes so often appear not to work. There is no single place to set this. A process receives the lowest of five values, and any one of them left at its default silently caps everything.
| Layer | Where | Default | Notes |
|---|---|---|---|
| Kernel global | fs.file-max | ~1M | Rarely the limit on modern kernels |
| systemd manager | DefaultLimitNOFILE | 1024 | Caps every service systemd starts |
| systemd unit | LimitNOFILE= | inherits above | Per-service override |
| PAM / login | pam_limits.so via /etc/security/limits.conf | 1024 | Only affects interactive shells, not services |
| Process | ulimit -n | inherited | What the app actually sees |
The classic mistake is editing /etc/security/limits.conf, testing by logging in over SSH where it works, and wondering why Nginx still runs out. SSH sessions go through PAM; systemd services do not. Our VPS comparison page is useful for picking a plan with enough memory to hold all those connections, but the limits themselves are yours to set.
Reading what a running process is actually allowed
Never guess. Ask the kernel what the process has, and ask systemd what it was told to grant.
# What the process actually has (works for any PID)
PID=$(pgrep -o nginx)
cat /proc/$PID/limits | grep -Ei 'open files|processes'
# What systemd thinks it granted
systemctl show nginx -p LimitNOFILE --value
# The interactive-shell view (different number, usually)
ulimit -n; ulimit -u
# Kernel-wide ceiling and current usage
sysctl fs.file-max
cat /proc/sys/fs/file-nr # allocated unused max
If /proc/$PID/limits shows 1024 and systemctl show also shows 1024, you know exactly which layer to fix. If systemd shows 65535 but the process shows 1024, the unit file or a drop-in is overriding it.
A configuration that covers every layer
# 1. Kernel global
cat >/etc/sysctl.d/99-file-max.conf </etc/security/limits.d/99-web.conf <<'EOF'
* soft nofile 65535
* hard nofile 65535
* soft nproc 32768
* hard nproc 32768
root soft nofile 65535
root hard nofile 65535
EOF
sysctl --system
systemctl daemon-reexec # required for manager-level limits
systemctl daemon-reexec is not optional — a plain daemon-reload does not re-apply the manager’s own limits to services started afterward on older systemd versions. And on distributions that include a limits.d/20-nproc.conf file, a later-sorted file wins, so name yours 99- to be sure. A common trap on CentOS-family images is root being excluded from the * wildcard, hence the explicit root lines.
Per-service overrides
For a service, use a drop-in rather than editing the package’s unit file, which will be overwritten on upgrade.
mkdir -p /etc/systemd/system/nginx.service.d
cat >/etc/systemd/system/nginx.service.d/limits.conf <<'EOF'
[Service]
LimitNOFILE=65535
LimitNPROC=32768
TasksMax=infinity
EOF
systemctl daemon-reload && systemctl restart nginx
cat /proc/$(pgrep -o nginx)/limits | grep 'open files'
TasksMax is easy to miss because it is a cgroup concept rather than a classic ulimit. With the systemd default of 15% of kernel.pid_max, a worker-spawning service hits Failed to fork: Resource temporarily unavailable long before nproc matters.
There is one more interaction worth knowing: fs.nr_open caps what any single process may request, and it is the ceiling that LimitNOFILE cannot exceed. If you set DefaultLimitNOFILE=1048576 while fs.nr_open remains at its lower default, systemd will accept the configuration and the service will still start with the smaller number. Check sysctl fs.nr_open before choosing a target, and raise both together rather than chasing the discrepancy later.
Choosing a number
- Web server: 2 file descriptors per proxied connection (client plus upstream), so 10,000 concurrent requests needs
LimitNOFILE=32768. - Database: each open table, each connection, and each temporary file consumes descriptors. MySQL’s
open_files_limitandtable_open_cacheinteract with the system limit. - Do not set
fs.file-maxwildly high and leave process limits low — the kernel-wide number is a safety net, not a per-process budget. - Memory matters: each socket also consumes kernel memory. Raising limits on a 512 MB instance does not create capacity that is not there.
Verifying under load
# Live descriptor usage for the web server
ls /proc/$(pgrep -o nginx)/fd | wc -l
# Which limit was hit, if any
grep -E 'Too many open files|Resource temporarily unavailable|accept4' \
/var/log/nginx/error.log /var/log/syslog
# System-wide allocation trend during a load test
watch -n1 'cat /proc/sys/fs/file-nr'
Run the load test again after the change. Descriptor count should rise to roughly your expected concurrency, error-log entries drop to zero, and file-nr‘s first column should stabilise rather than climb indefinitely. A count that climbs without plateauing under steady load is a descriptor leak in the application, which raising limits will only postpone.
When the counters are clean and connections still fail, the constraint has moved to memory or network throughput. See the full specs and pricing to match the plan to the concurrency you have now measured, rather than repeatedly raising limits on an instance that cannot physically sustain them.




Leave a Reply
You must be logged in to post a comment.