8000 EMFILE caused by file descriptor limit can lock up warp · Issue #1110 · yesodweb/wai · GitHub
[go: up one dir, main page]

Skip to content

EMFILE caused by file descriptor limit can lock up warp #1110

Open

Description

@mkleczek

it looks like handling EMFILE by warp:

handleFdExhaustion e = do

does more harm than good.

It is supposed to mitigate file descriptor exhaustion by introducing latency instead of erroring out. But the logic is based on a heuristic only taking into account local state (ie. only information available to warp) which is not enough very often.

Consider the scenario:

  1. The application starts warp
  2. The application is close to a file descriptor limit.
  3. warp accepts one connection
  4. warp receives EMFILE trying to accept the second and subsequent connections
  5. all connection requests wait for a single connection to finish. Once it happens, one of the waiting connection requests succeeds but all other keep waiting
  6. Because waitForDecreased reads the initial counter only once, all connections are waiting for connection counter to be decreased to 0 (see:
    waitForDecreased (Counter var) = do
    ) which might never happen.

Effectively, warp has locked itself and even increasing file descriptor limit (or releasing some file descriptors by other parts of the application) won't help.

It looks like it would be much better to simply return error upon EMFILE instead of trying to be "smart" and handle it.

Activity

  1. changed the title [-]EMFILE caused by global file descriptor can lock up warp[/-] [+]EMFILE caused by file descriptor limit can lock up warp[/+] on Aug 28, 2026
  2. Vlix commented on Aug 28, 2026

    @Vlix
    Contributor

    Another solution might be a(n async) check that starts running which tries to grab an FD from /dev/null (on Unix) or somewhere else (on Windows) every few milliseconds, and flips a flag once it works.

    Or just retrying, instead of waiting? Maybe with exponential backoff to a 1 sec cap.

  3. mkleczek commented on Aug 28, 2026

    @mkleczek
    ContributorAuthor

    Or just retrying, instead of waiting? Maybe with exponential backoff to a 1 sec cap.

    Yeah, I guess that's the best solution.

  4. kazu-yamamoto commented on Aug 31, 2026

    @kazu-yamamoto
    Contributor
    1. warp receives EMFILE trying to accept the second and subsequent connections
    2. all connection requests wait for a single connection to finish. Once it happens, one of the waiting connection requests succeeds but all other keep waiting

    I don't understand this part.
    Since handleFdExhaustion waits, acceptLoop stops at this point.
    It doesn't seem to me that "subsequent connections" would end up "waiting for a single connection to finish."

  5. mkleczek commented on Aug 31, 2026

    @mkleczek
    ContributorAuthor
    1. warp receives EMFILE trying to accept the second and subsequent connections
    2. all connection requests wait for a single connection to finish. Once it happens, one of the waiting connection requests succeeds but all other keep waiting

    I don't understand this part. Since handleFdExhaustion waits, acceptLoop stops at this point. It doesn't seem to me that "subsequent connections" would end up "waiting for a single connection to finish."

    You are right - that part is indeed missing that the first EMFILE stops the whole accept loop.

    Nevertheless, the issue is still there in case of long-living connections (such as HTTP 1.1 with keep-alives) - warp heuristics prevents accepting new connections when the number of available file descriptors is increased externally.

  6. Vlix commented on Sep 2, 2026

    @Vlix
    Contributor

    So shall we just remove the check (n < n0) part and just let it loop indefinitely on eMFILE? (like it does if we don't have any active connections)

    ...
         if | isErrno eMFILE -> do
    -        handleFdExhaustion e
    +        fdExhaustion <- readIORef fdRef
    +        when (fdExhaustion /= FdExhausted) $
    +            settingsOnException set Nothing $ E.toException e
    +        setFdExhaustion fdRef
             acceptNewConnection

    I don't see anything immediately wrong with this. It's one thread doing a forever that keeps throwing an eMFILE until it maybe doesn't. And it reports the exhaustion once, until it has gotten a new file descriptor.

  7. kazu-yamamoto commented on Sep 2, 2026

    @kazu-yamamoto
    Contributor

    It might be a good idea to slow down the accept loop if the rate of eMFILE errors exceeds a certain threshold. We can use the following for rate tracking:

    https://hackage-content.haskell.org/package/network-control-0.1.7/docs/Network-Control.html

  8. Vlix commented on Sep 4, 2026

    @Vlix
    Contributor

    It might be a good idea to slow down the accept loop if the rate of eMFILE errors exceeds a certain threshold. We can use the following for rate tracking:

    https://hackage-content.haskell.org/package/network-control-0.1.7/docs/Network-Control.html

    How would we use the Rate from that module? Would we use x <- getRate and if x == 1 we try again?
    (IIUC the Rate is initialized at 1 and gets incremented during the following second, and then is reset to 1 again, right?)

  9. kazu-yamamoto commented on Sep 5, 2026

    @kazu-yamamoto
    Contributor

    (IIUC the Rate is initialized at 1 and gets incremented during the following second, and then is reset to 1 again, right?)

    Right.

    • If the getRate value is less than our threshold, recurse.
    • Otherwise, call threadDelay with a duration of less than one second.
    • If the one-second unit for Rate seems too short, let's modify the library to allow specifying the duration.
  10. Vlix commented on Sep 24, 2026

    @Vlix
    Contributor

    I think I have an idea on how to implement the rate limiting.
    I'll be away this weekend, and I'm starting a new job next week, but I'll try to make a PoC somewhere next week.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      0