Skip to content

"drain" event listener leak when unpiping to response #135

Description

@zbjornson

This is sort of a weird scenario, but this test case fails:

  it('should clean up event listeners when res is unpiped to', function (done) {
    var listenerCount
    var server = createServer({ threshold: 0 }, function (req, res) {
      var times = 0
      var int = setInterval(function () {
        var rs = fs.createReadStream('does not exist')
        rs.on('error', function (e) {
          listenerCount = res.listenerCount('drain')
          rs.unpipe(res)
        })
        rs.pipe(res)
        if (times++ > 12) {
          clearInterval(int)
          res.end('hello, world')
        }
      })
    })

    request(server)
      .get('/')
      .set('Accept-Encoding', 'gzip')
      .expect(function () {
        assert.ok(listenerCount < 2)
      })
      .expect(200, done)
  })

I hit this in some code that retries creating a read stream until the source exists. We clean up from our side: rs.on("error", e => { rs.unpipe("res"); }). Seems like compression needs to be cleaning up its listeners when "unpipe" happens.

Activity

  1. self-assigned this
    on Mar 28, 2018
  2. matthiasg commented on Apr 17, 2018

    @matthiasg

    there is also a leak caused by mapping ServerResponse.once to ServerResponse.on so there is no easy way to wait for 'drain' events to happen by just using the response.once('drain', ...)

  3. changed the title [-]"drain" event listener leak[/-] [+]"drain" event listener leak when unpiping to response[/+] on Mar 7, 2019
  4. changed the title [-]"drain" event listener leak when unpiping to response[/-] [+]"drain" event listener leak when unpiping to response, using once("drain") or removeListener("drain")[/+] on Mar 7, 2019
  5. changed the title [-]"drain" event listener leak when unpiping to response, using once("drain") or removeListener("drain")[/-] [+]"drain" event listener leak when unpiping to response[/+] on Mar 7, 2019
  6. added 12 commits that reference this issue on Mar 7, 2019
    0aba544
    6c36399
    d5de7fe
    11b009b
    95cd840
    3e898c1
    c94d854
    e18114b
    398536a
    3bfb188
    9d85331
    1aa646c
  7. added 3 commits that reference this issue on Jul 16, 2020
    bbce4a6
    84b4229
    778f27d
  8. jesseskinner commented on Mar 7, 2022

    @jesseskinner

    If anyone came here looking for a workaround for the ability to call res.once('drain', ...) without have node warn about MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 drain listeners added to [Gzip]. Use emitter.setMaxListeners() to increase limit..

    This is a bit of a mess, but you can do something like this:

    let onDrain;
    
    // add a single drain listener early on
    res.on('drain', () => {
        if (onDrain) {
            // call the onDrain callback at most once
            onDrain();
            onDrain = undefined;
        }
    });
    
    // later, when you would normally call res.once('drain', ...)
    onDrain = () => {
        // resume your input stream or whatever
    };

    I hope that helps someone feeling stuck on this old issue.

  9. added a commit that references this issue on Sep 16, 2026
    ed45c49
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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions