Two programs, one path
Every packet that reaches Shieldify on ports 80 and 443 passes through two programs we wrote ourselves:
- Breakwave, an XDP program. XDP (eXpress Data Path) runs an eBPF program inside the network driver, on every packet as it arrives, before the Linux kernel allocates a socket buffer for it or runs it through the network stack. Ending a packet there is about as cheap as handling a packet gets in Linux.
- The edge, a reverse proxy written in Rust. It terminates TLS, speaks HTTP/1.1 and HTTP/2, runs our checks and the web application firewall, caches, and forwards what passes to the website's own server, the origin.
Between the two sits the kernel's TCP stack, which we leave as it is.
The edge decides, XDP enforces
XDP is fast because it sees little: a packet's addresses and ports, and the first bytes of a connection. It cannot see inside TLS, it cannot read a request, and it cannot tell one household from the next behind a shared address. The edge sees all of that: the whole request, the connection it came on, and how the client behaved across both.
So decisions are made in the edge, per client, on evidence from that client's own requests and connections. When the edge judges a client to be part of an attack, it writes the verdict into BPF maps, hash tables that the edge and the XDP program share. From the client's next packet on, XDP enforces the verdict in the driver.
Nothing is decided in XDP on its own. A client the edge has not judged is never dropped there: its packets go through to the edge, and the edge checks it. We do not drop traffic by country, by network or by reputation list, and during an attack we do not switch to letting only known clients in. The door stays open, and the evidence decides who is turned away.
What XDP does with a verdict
A verdict is narrow: it is about one attacker and the website it attacked. From then on XDP ends that attacker's packets to that website in the driver, before a TLS handshake or a request costs anything further up. Everyone else, on that website and on every other, passes as before.
Narrow matters because of who else is out there. Many addresses in these floods are home connections, and many home connections are shared by whole neighbourhoods behind carrier-grade NAT. A verdict about one device must not lock out the people next to it.
Twelve minutes, seen from the driver
On 23 October the tool behind our 20 October flood came back for a login page. For just under twelve minutes, Breakwave counted every packet that arrived on ports 80 and 443, and every packet it ended:
Show the numbers
| Minute (UTC) | Packets arrived | Ended at XDP | Share |
|---|---|---|---|
| 02:49 | 7,932,682 | 1,909,065 | 24.1% |
| 02:50 | 7,553,282 | 2,898,234 | 38.4% |
| 02:51 | 8,299,834 | 2,939,763 | 35.4% |
| 02:52 | 8,360,518 | 2,946,065 | 35.2% |
| 02:53 | 8,102,585 | 2,993,110 | 36.9% |
| 02:54 | 8,030,543 | 2,970,815 | 37.0% |
| 02:55 | 7,774,976 | 3,017,064 | 38.8% |
| 02:56 | 7,628,941 | 3,048,242 | 40.0% |
| 02:57 | 7,518,512 | 3,055,271 | 40.6% |
| 02:58 | 7,403,560 | 3,083,536 | 41.6% |
| 02:59 | 7,396,594 | 3,084,879 | 41.7% |
| 03:00 | 5,521,217 | 2,333,336 | 42.3% |
| Total | 91,523,244 | 34,279,380 | 37.5% |
Of 91.5 million packets, 34.3 million never left the network driver. In the flood's first minute XDP ended 24% of its packets; by the last it ended 42%, as the edge's verdicts added up.
The shorter 20 October flood looked the same from the driver: about 37.3 million packets in three and a half minutes, of which 13.3 million (36%) ended at XDP, and up to 43% in a single minute.
Why the rest still reaches the edge
If ending packets in the driver is so cheap, why did six in ten still go through to the edge? Because every device has to show itself before anything is decided about it. A client's first connection always reaches the edge, and these floods kept bringing new devices: on 20 October a new crowd arrived every minute, and on 23 October about 1.07 million addresses took part.
That is the price of never dropping a visitor we have not judged, and we pay it on purpose. It sets the edge's job: take the first requests of a million unknown devices, decide on each of them, and pass the verdict down before the device's next connection arrives.
Why we wrote the edge in Rust
Until 18 October 2026 our edge ran on OpenResty, nginx with LuaJIT. Behind it, in the background, we had been building our own proxy in Rust. On 18 October it took over, carrying the protection logic we had refined on OpenResty for months. Floods like these were the main reason for the move:
- One process, shared state. nginx runs separate worker processes, and they share state through shared-memory dictionaries guarded by locks. Under a flood every request read and wrote that state, and the workers waited on each other. Our edge keeps its state in one process, in structures built for many threads.
- HTTP/2 under our control. A flood is many streams on few connections. The edge decides on a stream as soon as its headers arrive, takes no more work from a connection it has already refused, and hands the client to XDP from the request path itself.
- Verdicts straight into the maps. The edge writes the BPF maps itself, with no helper process in between, so a verdict reaches the driver as soon as it is made.
- Memory safety where it matters most. The edge parses everything the internet sends it. Rust takes whole classes of memory bugs out of the code that does that.
What we leave out on purpose
No country or network blocks
These floods came from home connections in more than fifty countries, on the same networks real customers use. Blocking them blocks customers.
No "known clients only" mode
Under attack, a new visitor is still a visitor. Clients we have not judged are checked, never dropped.
No limits per address alone
Spread over a million addresses, each address sends its share slowly. A limit low enough to catch that also locks out people who share an address.
Data and method
The packet counts come from Breakwave's own per-minute counters: every packet to ports 80 and 443 that reached the program, and every packet it ended. The counts are exact. Times are in UTC and minutes are whole UTC minutes, so the first and last minutes of a flood include a few seconds of ordinary traffic. Request and address figures come from our two flood reports. We publish no addresses, and we do not name the website.