Raising File Descriptor and Process Limits on a VPS for High Connection Counts

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.

LayerWhereDefaultNotes
Kernel globalfs.file-max~1MRarely the limit on modern kernels
systemd managerDefaultLimitNOFILE1024Caps every service systemd starts
systemd unitLimitNOFILE=inherits abovePer-service override
PAM / loginpam_limits.so via /etc/security/limits.conf1024Only affects interactive shells, not services
Processulimit -ninheritedWhat 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_limit and table_open_cache interact with the system limit.
  • Do not set fs.file-max wildly 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