# 5. File-Descriptor Limitations

> Descriptor limits, sizing, system constraints, and troubleshooting

---

LLMS index: [llms.txt](/llms.txt)

---

<!-- Generated by scripts/generate-haproxy-docs.py from pinned upstream text. -->

In order to ensure that all incoming connections will successfully be served, HAProxy computes at
load time the total number of file descriptors that will be needed during the process's life. A
regular Unix process is generally granted 1024 file descriptors by default, and a privileged process
can raise this limit itself. This is one reason for starting HAProxy as root and letting it adjust
the limit. The default limit of 1024 file descriptors roughly allow about 500 concurrent connections
to be processed. The computation is based on the global maxconn parameter which limits the total
number of connections per process, the number of listeners, the number of servers which have a
health check enabled, the agent checks, the peers, the loggers and possibly a few other technical
requirements. A simple rough estimate of this number consists in simply doubling the maxconn value
and adding a few tens to get the approximate number of file descriptors needed.

Originally HAProxy did not know how to compute this value, and it was necessary to pass the value
using the "ulimit-n" setting in the global section. This explains why even today a lot of
configurations are seen with this setting present. Unfortunately it was often miscalculated
resulting in connection failures when approaching maxconn instead of throttling incoming connection
while waiting for the needed resources. For this reason it is important to remove any vestigial
"ulimit-n" setting that can remain from very old versions.

Raising the number of file descriptors to accept even moderate loads is mandatory but comes with
some OS-specific adjustments. First, the select() polling system is limited to 1024 file
descriptors. In fact on Linux it used to be capable of handling more but since certain OS ship with
excessively restrictive SELinux policies forbidding the use of select() with more than 1024 file
descriptors, HAProxy now refuses to start in this case in order to avoid any issue at run time. On
all supported operating systems, poll() is available and will not suffer from this limitation. It is
automatically picked so there is nothing to do to get a working configuration. But poll's becomes
very slow when the number of file descriptors increases. While HAProxy does its best to limit this
performance impact (eg: via the use of the internal file descriptor cache and batched processing), a
good rule of thumb is that using poll() with more than a thousand concurrent connections will use a
lot of CPU.

For Linux systems base on kernels 2.6 and above, the epoll() system call will be used. It's a much
more scalable mechanism relying on callbacks in the kernel that guarantee a constant wake up time
regardless of the number of registered monitored file descriptors. It is automatically used where
detected, provided that HAProxy had been built for one of the Linux flavors. Its presence and
support can be verified using "haproxy -vv".

For BSD systems which support it, kqueue() is available as an alternative. It is much faster than
poll() and even slightly faster than epoll() thanks to its batched handling of changes. At least
FreeBSD and OpenBSD support it. Just like with Linux's epoll(), its support and availability are
reported in the output of "haproxy -vv".

Having a good poller is one thing, but it is mandatory that the process can reach the limits. When
HAProxy starts, it immediately sets the new process's file descriptor limits and verifies if it
succeeds. In case of failure, it reports it before forking so that the administrator can see the
problem. As long as the process is started by as root, there should be no reason for this setting to
fail. However, it can fail if the process is started by an unprivileged user. If there is a
compelling reason for *not* starting haproxy as root (eg: started by end users, or by a
per-application account), then the file descriptor limit can be raised by the system administrator
for this specific user. The effectiveness of the setting can be verified by issuing "ulimit -n" from
the user's command line. It should reflect the new limit.

Warning: when an unprivileged user's limits are changed in this user's account, it is fairly common
that these values are only considered when the user logs in and not at all in some scripts run at
system boot time nor in crontabs. This is totally dependent on the operating system, keep in mind to
check "ulimit -n" before starting haproxy when running this way. The general advice is never to
start haproxy as an unprivileged user for production purposes. Another good reason is that it
prevents haproxy from enabling some security protections.

Once it is certain that the system will allow the haproxy process to use the requested number of
file descriptors, two new system-specific limits may be encountered. The first one is the
system-wide file descriptor limit, which is the total number of file descriptors opened on the
system, covering all processes. When this limit is reached, accept() or socket() will typically
return ENFILE. The second one is the per-process hard limit on the number of file descriptors, it
prevents setrlimit() from being set higher. Both are very dependent on the operating system. On
Linux, the system limit is set at boot based on the amount of memory. It can be changed with the
"fs.file-max" sysctl. And the per-process hard limit is set to 1048576 by default, but it can be
changed using the "fs.nr_open" sysctl.

File descriptor limitations may be observed on a running process when they are set too low. The
strace utility will report that accept() and socket() return "-1 EMFILE" when the process's limits
have been reached. In this case, simply raising the "ulimit-n" value (or removing it) will solve the
problem. If these system calls return "-1 ENFILE" then it means that the kernel's limits have been
reached and that something must be done on a system-wide parameter. These trouble must absolutely be
addressed, as they result in high CPU usage (when accept() fails) and failed connections that are
generally visible to the user. One solution also consists in lowering the global maxconn value to
enforce serialization, and possibly to disable HTTP keep-alive to force connections to be released
and reused faster.
