Share on social
October 20, 2026
Lorem ipsum
TL;DR: We implemented PortSwigger's research for 100k requests per second in a forked version of ffuf. In the process, we added a web UI to ffuf. Our fork can be obtained here: https://github.com/assetnote/h3fuzz
There was some research that the PortSwigger team released in late September that revealed that they were able to reach 100k requests per second over WiFi using HTTP/3. This caught my attention as a hacker that has always tried to find speed with fuzzing attempts. The context of the blog post stuck to how this worked with Turbo intruder inside Burp Suite, and how it could be used to perform time of check time of use (TOCTOU) attacks.
While TOCTOU attacks are a use case that fuzzing can help with, most of my time as an attacker usually goes into discovery efforts rather than a specific attack. The fact that the PortSwigger team was able to reach 100k requests per second in Turbo Intruder really caught my attention as I felt that this same technology could be used towards fuzzing for discovery efforts instead (bruteforcing files and folders on webservers).
Being inspired from their research, I went ahead and forked the ffuf project and started implementing their HTTP/3 strategy from Turbo Intruder inside ffuf. There were a few key things that had to be implemented:
HEAD or Range requests to limit the bandwidth and disruption on the server.While Turbo Intruder is a treasured feature of Burp Suite, I wanted to achieve the speed described in their blog post with open tooling that is commonly used for discovery efforts when performing blackbox testing.
What I ultimately found at the end of this project was that while the theory of reaching 100k requests per second is viable, in practice, it's quite rare to find web servers that can comfortably handle 100k requests per second, let alone web servers that even have HTTP/3 configured in a way to support this request load.
Before I even started work on this project, I looked into how widely supported HTTP/3 is, and to be honest, it looked quite promising:
From: https://en.wikipedia.org/wiki/HTTP/3
On 7 June 2021, LiteSpeed Web Server (and OpenLiteSpeed) 6.0.2 was released and became the first version to enable HTTP/3 by default.
Caddy web server v2.6.0 (released 20 September 2022) has HTTP/3 enabled by default.
nginx supports HTTP/3 since 1.25.0 (released 23 May 2023). A technology preview of nginx with HTTP/3 support was released in June 2020. Binary packages of nginx with HTTP/3 support have been released in February 2023.
Cloudflare distributes a patch for nginx that integrates the quiche HTTP/3 library into it.
Microsoft IIS support for HTTP/3 is enabled natively with Windows Server 2022/Windows 11.
HAProxy supports HTTP/3 over QUIC since version 2.6 released on 31 May 2022.
Nimble Streamer supports HTTP/3 since 4.1.8-1 for HTTP-based protocols.
Reading through the above, you might think that it's a widely supported feature, but it's only when you start digging through the details that you start to understand that HTTP/3 is not enabled by default but has to be opted into for pretty much every web server implementation of it.
For example, Nginx requires you to define a UDP listener for the QUIC protocol to support HTTP/3 explicitly. IIS requires you to set a specific registry key for it to actually be enabled. Apache doesn't even support HTTP/3 and has no plans to as it would involve a huge rewrite. The dream of performing 100k requests per second for discovery efforts quickly dies out in practice when facing the reality of how poorly supported it is in down stream servers.
Even moving past all of these blockers to seeing 100k requests per second for discovery efforts being possible, can servers even handle 100k requests per second without being DoS'd off the planet? To be honest, not really. You're either being blocked by a WAF, or you're ultimately DoS'ing the underlying server.
From my testing, there are some edge cases where 100k requests per second was achieved without leading to a DoS while still being useful, and that's mostly been infrastructure on Google Cloud CDNs.
All of this gives a really narrow scope for where this capability is actually worthwhile from an offensive security perspective, whether it's TOCTOU attacks, or discovery related fuzzing.
Despite all of this, we took some time in the last week to fix the things that annoy us the most about manual fuzzing for discovery efforts in our fork of ffuf. The project we are releasing today has been built on the ffuf engine with a huge amount of respect for Joona Hoikkala and the ffuf team, as well as a huge amount of respect to the PortSwigger team for their work on supporting HTTP/3 in Turbo Intruder.
We're releasing h3fuzz, which includes an interactive web UI on top of ffuf's engine, as well as implements PortSwigger's HTTP/3 research (instead using quic-go instead of kwik). The work was implemented with GPT 5.6 Sol Ultra, with frequent check ins, bug fixes after thorough testing and QA. To achieve 100k requests per second, we used a local harness, looping until 120k requests per second was achieved. There is documentation in the docs/ folder inside the repo with how this benchmark worked and how it was achieved by GPT.
To understand why HTTP/3 makes such a difference, it helps to look at what ffuf actually does on the wire. Over HTTP/1.1 every worker holds one TCP connection with one request in flight on it, so the request rate is bounded by threads / latency. At the default 40 threads and a 50 ms round trip that is roughly 800 requests per second, and the only way up is more connections. HTTP/2 multiplexes requests over a single connection, but it still runs on TCP, so one lost packet blocks every stream on that connection until it is retransmitted, and servers cap the stream count anyway (nginx defaults to 128).
QUIC (RFC 9000) removes both limitations. Each HTTP/3 request (RFC 9114) is its own stream, streams are delivered independently so packet loss only stalls the stream it hit, and opening one is just a matter of sending its first frame. A single connection comfortably carries 256 requests in flight, which is the figure Turbo Intruder settled on after trying 100, 256 and 512, and several connections can run side by side. From there it is Little's law: requests in flight equals rate multiplied by latency. For 100,000 requests per second at 10 ms you need 1,000 requests in flight, or four connections; at 50 ms it is 5,000, or twenty.

h3fuzz does this with quic-go rather than Kwik, which Turbo Intruder uses. One detail that is easy to miss is that quic-go's http3.Transport caches a single connection per origin, so handing it 5,000 concurrent requests would funnel all of them through one packet-processing loop. We create a separate transport for every connection in the pool instead (http3_pool.go). The pool starts at 8 connections of 256 streams each and is resized by a controller we ported from Turbo Intruder's AdaptivePolicy: it samples every 250 ms over one second windows, does nothing unless the pool was saturated for at least 75% of the window, doubles the connection count while throughput improves by 3% or more, binary-searches between the best and the first non-improving count once it stops, backs off when requests start failing, and probes 10% higher after 30 seconds of steady state, up to 64 connections.
The next problem is knowing whether the server speaks HTTP/3 at all. The obvious signal is the Alt-Svc header (RFC 7838), but in practice it is unreliable in both directions: CDNs advertise h3 in front of origins where UDP 443 is filtered, and some servers speak HTTP/3 without advertising it. h3fuzz records the header and then does a direct QUIC handshake to the port in -u (http3_fingerprint.go). The connection has to negotiate ALPN h3, receive the server's SETTINGS frame (RFC 9114 §7.2.4), complete a request, complete a second one on the same connection, and then complete 16 at once. You can run this step on its own:
$ ./h3fuzz -u https://cloudflare-quic.com/FUZZ -http3-fingerprint
{
"target": "https://cloudflare-quic.com/FUZZ",
"supported": true,
"high_rate_ready": true,
"alpn": "h3",
"quic_version": "v1",
"http_version": "HTTP/3.0",
"status_code": 200,
"connection_reused": true,
"concurrent_streams_completed": 16,
"concurrent_streams_attempted": 16,
"remote_addr": "104.18.27.14:443",
"elapsed_ms": 650
}
If any of those checks fail, automatic mode falls back to the normal TCP transport, which negotiates HTTP/2 through ALPN or ends up on HTTP/1.1. This is decided once before the scan starts; a scan that began on HTTP/3 is never quietly moved to TCP halfway through (protocol-selection.md).
Finally there is response size. PortSwigger's advice is to use HEAD or Range: bytes=-1 (RFC 9110 §14) so the server returns as little as possible, which is correct, but for discovery you cannot assume a server honours either one. h3fuzz sends three plain GET requests first (one path that probably exists and two that certainly don't) and repeats them as HEAD; it only switches if the status code, redirect target, content type and declared length match in all three cases. If HEAD fails that test, Range: bytes=-1 has to return a one byte 206 whose Content-Range total equals the full length for the hit while leaving both misses unchanged, so a soft-404 that answers 200 to everything is rejected here. The optimisation is off whenever a matcher, filter or scraper needs the body, the original size is reconstructed from Content-Length or Content-Range so -fs keeps working, and a response that no longer matches the behaviour seen in preflight is retried as a full GET (response_optimization.go).
There is one change we had to make to quic-go itself. On the HEAD path the QPACK (RFC 9204) header block is built once per scan and only :path is encoded per request; the patch lives in third_party/ and can be verified against upstream byte for byte. What we do not have is Turbo Intruder's hand-written request encoder and its dynamic table reuse for :authority, which is the main reason its hot path is leaner than ours. The full comparison, with the measurements behind every constant above and the optimisations we tried and rejected, is in http3-100k-analysis.md.
The biggest wins from this project aren't really from the fact that we can support HTTP/3, but rather the quality of life improvements we gain after adding a web UI to ffuf.
Whenever I am performing content discovery, it often fits within a preset that I have shell aliases for (different ffuf commands depending on the backend I know I am bruteforcing). Depending on the backend, it sets different extensions that replace the %EXT% inside the wordlist being used (Dirsearch compatibility mode). This is really trivial in the target setup wizard in the web UI:

Usually when we are performing manual content discovery efforts, we often are plagued with responses that we want to readily filter out. We know that this is something you can do after starting a scan in ffuf's CLI version, but the previous results still take up most of the terminal. In our web UI, filtering bad responses works through intuitive filters and retroactively, to clean up your results pane.
Another common reality when fuzzing manually is that you want to perform several content discovery sessions at once. In CLI world, that means either using screen or tmux and switching between windows. This is often painful when managing a large number of sessions in the terminal, but is incredibly easy in a web UI. The web UI we built optimises for this use case.
Quickly going to, or exporting results, is also a pain when dealing with results from the terminal, but not from a web UI, which is another quality of life feature that I know people who do a lot of content discovery will really appreciate when using our tool.
Here's a short demo of h3fuzz (and its web UI):