http: limit connected HTTPRemoteClients #35730

pull pinheadmz wants to merge 4 commits into bitcoin:master from pinheadmz:http-client-limit changing 7 files +186 −22
  1. pinheadmz commented at 2:22 PM on July 15, 2026: member

    Introduces a new configuration option -rpcmaxconnections with default value 128. This is used to limit the number of simultaneous HTTPClient connected to the HTTPServer. When the limit is reached, new pending connections remain queued in the kernel's socket buffer. Those connections have complete TCP handshakes with the kernel but do not occupy any application memory.

    The previous libevent-based HTTP server had no limit on connections but it did have a limit on the kernel socket queue:

    https://github.com/libevent/libevent/blob/e7ff4ef2b4fc950a765008c18e74281cdb5e7668/http.c#L3510

    	if (listen(fd, 128) == -1) {
    

    The current HTTP server, like the p2p server, uses a platform constant here:

    https://github.com/bitcoin/bitcoin/blob/b6becf3534c7b7f1b4d356a8f6113d62b6dd05bf/src/httpserver.cpp#L743

    (on my macOS SOMAXCONN is 128 but on my Debian machine it's 4096)

    The default of 128 was chosen as a reasonable upper bound for RPC use cases. Systems designed to handle more simultaneous HTTP connections than this (previously relying on the absence of a limit) can adjust the setting.

    File descriptors

    Because of the connection limit, we can now account for the maximum number of file descriptors needed by the HTTP server. This addresses several issues (#11368 #11322 maybe #27732) that could have been fixed by a PR waiting in vain for a libevent release (#27731).

    Bonus performance improvement

    The new limit is managed in a loop that drains the kernel's socket queue with accept(). All pending connections from the queue (up to the limit) are processed in one single call to SocketHandlerListening(). The previous code would only accept one connection from the queue on each I/O loop tick, with a SELECT_TIMEOUT (50ms) sleep between each.

  2. DrahtBot added the label RPC/REST/ZMQ on Jul 15, 2026
  3. DrahtBot commented at 2:23 PM on July 15, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35730.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    ACK janb84
    Concept ACK furszy, fjahr, brunoerg, stickies-v, willcl-ark

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #35592 (http: check rpcallowip immediately after accepting connection by pinheadmz)
    • #35037 (ipc: support per-address max-connections options on -ipcbind by enirox001)
    • #34978 (init: reserve file descriptors for IPC connections by enirox001)
    • #34794 (rest: add Cache-Control headers to REST responses by w0xlt)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. furszy commented at 2:26 PM on July 15, 2026: member

    Concept ACK

  5. fjahr commented at 2:38 PM on July 15, 2026: contributor

    Concept ACK

  6. brunoerg commented at 3:32 PM on July 15, 2026: contributor

    Concept ACK

  7. in src/httpserver.cpp:1231 in 937110f08f outdated
    1227 | @@ -1228,6 +1228,7 @@ bool InitHTTPServer()
    1228 |      g_http_server = std::make_unique<HTTPServer>(MaybeDispatchRequestToWorker);
    1229 |  
    1230 |      g_http_server->SetServerTimeout(std::chrono::seconds(gArgs.GetIntArg("-rpcservertimeout", DEFAULT_HTTP_SERVER_TIMEOUT)));
    1231 | +    g_http_server->SetMaxConnections(std::max(gArgs.GetArg<int>("-rpcmaxconnections", DEFAULT_MAX_HTTP_CONNECTIONS), 1));
    


    brunoerg commented at 5:11 PM on July 15, 2026:

    937110f08f00421f6af7cccf627abab79c3c291b: nit: Is it worth to add an upper bound limit on it as well? Just to avoid a huge int value on it due to min_required_fds.


    pinheadmz commented at 5:50 PM on July 16, 2026:

    If the platform can't provide enough file descriptors, bitcoin will raise an InitError. But I will make sure the setting is int-safe in init.cpp

  8. in src/init.cpp:1073 in 36da6684cc outdated
    1069 | +    int nRPCBind = std::max(args.GetArgs("-rpcbind").size(), size_t(2));
    1070 | +    // HTTP server connected client sockets
    1071 | +    int rpc_max_connections = std::max(args.GetArg<int>("-rpcmaxconnections", DEFAULT_MAX_HTTP_CONNECTIONS), 1);
    1072 |      // Reserve enough FDs to account for the bare minimum, plus any manual connections, plus the bound interfaces
    1073 | -    int min_required_fds = MIN_CORE_FDS + MAX_ADDNODE_CONNECTIONS + nBind;
    1074 | +    int min_required_fds = MIN_CORE_FDS + MAX_ADDNODE_CONNECTIONS + nBind + nRPCBind + rpc_max_connections;
    


    brunoerg commented at 5:21 PM on July 15, 2026:

    36da6684cc61a960064aa369ecfe3a86610e2566: If -server=0 I don't think we should increase min_required_fds this way.


    willcl-ark commented at 10:30 AM on July 16, 2026:

    In 36da6684cc61a960064aa369ecfe3a86610e2566

    Is it OK to add these even if we are running with -server=0?


    willcl-ark commented at 10:44 AM on July 16, 2026:

    Oh, reading the earlier review I see bruno has flagged this too. I should have perhaps done that first :'(


    pinheadmz commented at 6:47 PM on July 17, 2026:

    Good call, will ignore HTTP fd's if server=0 and cover with a test. Adding this in an extra commit.

  9. in test/functional/interface_http.py:578 in 937110f08f
     591 | -        ):
     592 | -            # Fill connection slots
     593 | -            for i in range(1, MAX_HTTP_CONNECTIONS + 1):
     594 | +        for comment, extra_args, limit in [
     595 | +            ("default (128)", [], 128),
     596 | +            ("-rpcmaxconnections=18", ["-rpcmaxconnections=18"], 18)
    


    brunoerg commented at 5:31 PM on July 15, 2026:

    937110f08f00421f6af7cccf627abab79c3c291b: Perhaps it could set -rpcservertimeout=0 so that the connections filling the slots are never disconnected while the test is still running.


    pinheadmz commented at 2:55 PM on July 16, 2026:

    Oh yeah great idea! Adding -rpcservertimeout=0

  10. in test/functional/interface_http.py:593 in 937110f08f outdated
     606 | +
     607 | +            MAX_HTTP_CONNECTIONS = limit
     608 | +            connections = []
     609 | +
     610 | +            # No errors logged
     611 | +            with self.node.assert_debug_log(
    


    brunoerg commented at 5:43 PM on July 15, 2026:

    937110f08f00421f6af7cccf627abab79c3c291b: Curious fact: method=invalidrpc_1 is a substring of the log line for request 10-19 and 100–127 - e.g. invalidrpc_1 is a prefix of invalidrpc_128. So the check "method=invalidrpc_1" in log returns True even if request 1 was never sent.


    pinheadmz commented at 2:59 PM on July 16, 2026:

    Good point, I'll include a trailing space so ...=1 " is distinct from ...=10 "

  11. stickies-v commented at 9:52 AM on July 16, 2026: contributor

    Concept ACK

  12. in src/init.cpp:1071 in 36da6684cc outdated
    1064 | @@ -1065,8 +1065,12 @@ bool AppInitParameterInteraction(const ArgsManager& args)
    1065 |      const size_t max_private{args.GetBoolArg("-privatebroadcast", DEFAULT_PRIVATE_BROADCAST)
    1066 |                               ? MAX_PRIVATE_BROADCAST_CONNECTIONS
    1067 |                               : 0};
    1068 | +    // HTTP server listen sockets: by default two (IPv4 and IPv6 loopback), or one per -rpcbind entry
    1069 | +    int nRPCBind = std::max(args.GetArgs("-rpcbind").size(), size_t(2));
    1070 | +    // HTTP server connected client sockets
    1071 | +    int rpc_max_connections = std::max(args.GetArg<int>("-rpcmaxconnections", DEFAULT_MAX_HTTP_CONNECTIONS), 1);
    


    willcl-ark commented at 10:38 AM on July 16, 2026:

    In 36da6684cc61a960064aa369ecfe3a86610e2566

    Don't percieve this as introduced here, but what happens if -rpcconnections > INT_MAX?


    pinheadmz commented at 8:50 PM on July 16, 2026:

    Good catch, I'll add a check to ensure that the total file descriptors the user needs doesn't overflow int, which is the type expected by RaiseFileDescriptorLimit()

    But you're also right, this isn't introduced here. For example -rpcthreads=999999999999 would get clamped at 2^32, but still crash while actually trying to start that many threads.

    New behavior for this setting (with ulimit -n 165):

    # Exceed OS limit
    --> bcd -rpcmaxconnections=4
    Error: Not enough file descriptors available. 165 available, 166 required.
    --> bcd -rpcmaxconnections=3 -bind=::1:10000 -bind=::1:20000
    Error: Not enough file descriptors available. 165 available, 166 required.
    
    # Setting would overflow max int before even checking OS
    --> bcd -rpcmaxconnections=99999999999999999
    Error: Too many file descriptors requested. Try lower values for -rpcmaxconnections or -maxconnections, or fewer settings of -rpcbind, -bind and -whitebind
    
  13. in src/httpserver.cpp:925 in 962ff07a61 outdated
     925 | +            // Stop early if the kernel queue is empty (AcceptConnection returns null)
     926 | +            // or if accepting the last connection brought us to the limit.
     927 | +            while (GetConnectionsCount() < MAX_HTTP_CONNECTIONS) {
     928 | +                CService addr_accepted;
     929 | +                auto sock_accepted{AcceptConnection(*sock, addr_accepted)};
     930 | +                if (!sock_accepted) break;
    


    willcl-ark commented at 10:39 AM on July 16, 2026:

    In 962ff07a61eedcca02ce479101ea4b5d582fac72

    This is nice!

    could (but perhaps also should?) we add a test that opens multiple connections concurrently, verifies the accept loop drains them up to the limit, and then confirms a queued connection is accepted after one slot is freed? The current test opens connections sequentially AFAICT.


    pinheadmz commented at 2:51 PM on July 16, 2026:

    The functional test opens connections one at a time in a single thread, but then they all remain open concurrently. While open, one extra connection is tested and expected to time out. What I don't have yet is testing that one extra connection with a much longer time out, does finally get accepted and responded to... so I will add that.

  14. willcl-ark commented at 10:43 AM on July 16, 2026: member

    Concept ACK!

  15. b-l-u-e commented at 3:23 PM on July 16, 2026: contributor

    Tested on Windows 11 Pro with native MSVC bitcoind v31.99.0.

    -rpcmaxconnections=4 -rpcservertimeout=30 connection queued okay no refusal or reset, no response or log activity while all slots were occupied then HTTP 200 about 1.3 ms after a slot was freed. Idle slots were reclaimed after about 30.4s.

    The Backlog results:

    • 32attempts and 64 attempts: all connected
    • 128 attempts: 104 connected, 24 timed out
    • 256 attempts: 204 connected, 52 timed out

    and also all failures were TimeoutError; none refused or reseted

  16. http: limit connected clients to 128 548e995e57
  17. http: configure simultaneous connection limit with -rpcmaxconnections 3680a95b55
  18. init: account for maximum file descriptors needed by HTTP a08f67791b
  19. init: do not count file descriptors for HTTPServer if -server=0 779e722fba
  20. in test/functional/interface_http.py:607 in 36da6684cc
     602 | +
     603 | +            # Over the limit, expect rejection
     604 | +            with self.node.assert_debug_log(
     605 | +                expected_msgs = [],
     606 | +                unexpected_msgs = ["method=never_accepted"]
     607 | +            ):
    


    b-l-u-e commented at 3:38 PM on July 16, 2026:

    here comment says over the limit, expect rejection but when at capacity the server queues in the listen backlog and the client hangs. i think comment should say like hang or timeout


    pinheadmz commented at 7:09 PM on July 17, 2026:

    You're right! My original approach disconnected excessive clients instead of leaving them in the kernel queue. I'll fix the comment.

  21. pinheadmz force-pushed on Jul 17, 2026
  22. pinheadmz commented at 7:25 PM on July 17, 2026: member

    push to 779e722fba36098deef75329346b132ad1972243

    Address review feedback, mostly improving test coverage but also improved handling of excessive rpcmaxconnections values and not reserving file descriptors if the HTTP server is disabled.

  23. in doc/release-notes-35182.md:21 in 779e722fba
      13 | @@ -14,3 +14,8 @@ Certain HTTP edge cases will observe different behavior to be more RFC-compliant
      14 |  - "Line Folding" is rejected (whitespace at start of a header line)
      15 |  - Tolerate `%` at the end of requested URLs
      16 |  - Multiple "Content-Length" headers with different values are rejected
      17 | +
      18 | +A new configuration option `-rpcmaxconnections` (default `128`) limits the
      19 | +number of simultaneously connected HTTP clients to the server. The application
      20 | +will now attempt to reserve file descriptors for the HTTP server sockets. If your
      21 | +system has limited resources, consider using a lower setting.
    


    b-l-u-e commented at 3:44 AM on July 20, 2026:

    nit:

    diff --git a/doc/release-notes-35182.md b/doc/release-notes-35182.md
    index d7c22e2f03..2c05b5dc3d 100644
    --- a/doc/release-notes-35182.md
    +++ b/doc/release-notes-35182.md
    @@ -1,5 +1,4 @@
    -HTTP: RPC / REST
    -----------------
    +## HTTP: RPC / REST
     
     The HTTP server has been rewritten from scratch to replace libevent. (#35182)
     
    @@ -16,6 +15,9 @@ Certain HTTP edge cases will observe different behavior to be more RFC-compliant
     - Multiple "Content-Length" headers with different values are rejected
     
     A new configuration option `-rpcmaxconnections` (default `128`) limits the
    -number of simultaneously connected HTTP clients to the server. The application
    -will now attempt to reserve file descriptors for the HTTP server sockets. If your
    -system has limited resources, consider using a lower setting.
    +number of simultaneously connected HTTP clients to the server. When the limit
    +is reached, additional clients are not immediately rejected with an HTTP
    +error the connection may wait until a slot frees or until the client itself
    +times out. The application will also attempt to reserve file descriptors for
    +the HTTP server sockets. If your system has limited resources, consider using
    +a lower `-rpcmaxconnections` setting.
    

    pinheadmz commented at 1:35 PM on July 21, 2026:

    I think adding a line about queing connections after the server is full is justified, will add on next rebase

  24. b-l-u-e commented at 4:13 AM on July 20, 2026: contributor

    so far we have a test that covers connections one at a time..Could we add another test that opens several connections at once?

  25. pinheadmz commented at 1:37 PM on July 21, 2026: member

    so far we have a test that covers connections one at a time..Could we add another test that opens several connections at once?

    Not sure what you mean, check_connection_limit() in interface_http opens 128 connections then opens an additional connection two times (once expecting timeout, another waiting until a slot opens). Those connections are opened in a single python thread, but then they are all open at the same time, occupying server slots, file descriptors, sockets, etc. So I suppose the test could be modified to open those 128 connections in simultaneous parallel threads? But I don't think that would improve the test coverage.

  26. janb84 commented at 12:21 PM on July 22, 2026: contributor

    cr ACK 779e722fba36098deef75329346b132ad1972243

    LGTM!

    During review I discovered that ClientAllowed() does not actively disconnects the client, but awaits the timeout / disconnect from the client. The current PR limits the number of active connections which makes waiting for timeouts / client disconnects a (increased) DDOS vector, I have created a followup PR #35772 to address this. #35592 will mitigate the issue.

  27. DrahtBot requested review from fjahr on Jul 22, 2026
  28. DrahtBot requested review from willcl-ark on Jul 22, 2026
  29. DrahtBot requested review from brunoerg on Jul 22, 2026
  30. DrahtBot requested review from stickies-v on Jul 22, 2026
  31. DrahtBot requested review from furszy on Jul 22, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-07-25 06:50 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me