MacOS: Too Many Open Files

elixir macos
Posted on: 2022-08-29

(Published via time machine. Just kidding, my blog publication tooling was broken and it took me a long time to get around to fixing it.)

Today I was trying to make a large number of socket connections to a local Phoenix app (on MacOS 12.3 Monterey) and was running into limits on the maximum mumber of file descriptors (sockets in this case).

Specifically, I would see [warning] Ranch acceptor reducing accept rate: out of file descriptors and lots of [error] [label: {:erl_prim_loader, :file_error}, report: 'File operation error: emfile. Target: /path/to/Elixir.Phoenix.Endpoint.Cowboy2Handler.beam. Function: get_file. Process: code_server.']. (emfile means out of file descriptors according to man 2 intro.)

After much reading and asking and experimenting, this seems to be the deal.

There are two limits on the number of file descriptors on MacOS: shell limits and system limits. I think that if a process hits either of those limits (the lower of the two), it will have errors, although I’m less sure about the system limits.

First, the shell limits. ulimit -a shows them all, like:

-t: cpu time (seconds)              unlimited
-f: file size (blocks)              unlimited
-d: data seg size (kbytes)          unlimited
-s: stack size (kbytes)             8176
-c: core file size (blocks)         0
-v: address space (kbytes)          unlimited
-l: locked-in-memory size (kbytes)  unlimited
-u: processes                       5333
-n: file descriptors                200000

…or one can use ulimit -n to just see the file descriptor limit.

Hitting the file descriptor limit caused this warning: Ranch acceptor reducing accept rate: out of file descriptors

and a bunch of associated errors.

According to this thread:

This is not a bug in Cowboy (or Ranch), Ranch just shows a message when the OS refuses to give more file descriptors.

ulimit -n 524288 will set the limit for a given shell session; adding this to .zshrc (or .bashrc or whatever the shell uses) will set it for all new sessions.

Second, the are system-wide limits. I’m less confident about the effect of hitting a system-wide limit because the behavior was less consistent for me. I’m pretty sure I saw an error about the system limit on maxfiles at one point, but I couldn’t reliably reproduce it; maybe that has something to do with the soft vs hard limits. I did see weird behavior during this experiment, like [error] Postgrex.Protocol (#PID<0.420.0>) failed to connect: ** (DBConnection.ConnectionError) tcp connect (localhost:5432): can't assign requested address - :eaddrnotavail or [error] Postgrex.Protocol (#PID<0.419.0>) disconnected: ** (DBConnection.ConnectionError) tcp recv (idle): timeout or being unable to launch a web request in the browser or via curl 😱. All of which makes me think that setting the system limits higher is a good idea.

According to man launchctl, launchctl limit shows all the system-wide limits as found via getrlimit(2). Output looks like:

cpu         unlimited      unlimited
filesize    unlimited      unlimited
data        unlimited      unlimited
stack       8372224        67092480
core        0              unlimited
rss         unlimited      unlimited
memlock     unlimited      unlimited
maxproc     5333           8000
maxfiles    256            unlimited

(Aisde 1: One can also use launchctl limit maxfiles to see just that one.)

(Aside 2:

Similar outp\ut can be seen for the “kernel systectl parameters”:

~: sysctl -A | grep maxfiles
kern.maxfiles: 245760
kern.maxfilesperproc: 122880

)

The first number in the launchctl limit output is the “soft” limit and the second the “hard” limit. What’s the difference? According to man 2 setrlimit:

A resource limit is specified as a soft limit and a hard limit. When a soft limit is exceeded a process may receive a signal (for example, if the cpu time or file size is exceeded), but it will be allowed to continue execution until it reaches the hard limit (or modifies its resource limit).

These limits can be modified temporarily with a command like sudo launchctl limit maxfiles 524288 524288 (if you pass unlimited as the second argument, it ignores the command.)

Be warned ⚠️ that setting this value too low or too high can cause system-wide problems, and that it is not possible to specify unlimited as the hard limit, even though that’s the default. According to this post, unlimited means INT_MAX, which is equal to 2147483647 (a 32-bit SIGNED INT), and setting it any higher will cause the number to be interpreted as negative and crash MacOS.

One can make those limits persist restarts by creating /Library/LaunchDaemons/limit.maxfiles.plist to look like this:

# /Library/LaunchDaemons/limit.maxfiles.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
      <string>limit.maxfiles</string>
    <key>ProgramArguments</key>
    <array>
      <string>launchctl</string>
      <string>limit</string>
      <string>maxfiles</string>
      <string>524288</string>
      <string>524288</string>
    </array>
    <key>RunAtLoad</key>
    <true />
    <key>ServiceIPC</key>
    <false />
  </dict>
</plist>

Be sure to chown root:wheel limit.maxfiles.plist and set file permissions -rw-r–r– with chmod 644 limit.maxfiles.plist.

I got some of this information from this article.