The handshake has a gap in it
Every TCP connection starts with three packets. The client sends a SYN. The server replies SYN-ACK and reserves state for a connection that does not exist yet. The client confirms with an ACK, and only then is the connection established and handed to your application.
The gap is the middle step. Between sending the SYN-ACK and receiving the ACK, the server is holding a half-open connection: a kernel structure sitting in a fixed-size queue — the SYN backlog — waiting for a third packet that may never come. A SYN flood is nothing more than the decision never to send it.
The attacker sends SYN after SYN and ignores every reply. Each one takes a slot. Linux retransmits the SYN-ACK five times by default, backing off 1, 2, 4, 8, 16 and 32 seconds, so one forgotten packet can occupy a slot for roughly a minute. When the backlog fills, the kernel's only remaining move is to drop incoming SYN packets — including the ones from real visitors. Those visitors do not see an error page. Nothing answers at all, and the browser spins until it gives up, which is why a SYN flood is so often reported as "the site is down" rather than as an attack.
The economics are the whole attack
| The attacker pays | You pay | |
|---|---|---|
| Per handshake | One ~60-byte packet | A kernel structure held up to ~63 seconds |
| State kept | None — the packet is forgotten as it leaves | One backlog slot, plus a conntrack entry on any stateful firewall in the path |
| Work done | Fire and forget | Up to five SYN-ACK retransmissions, sent into the void |
| Identity | A source address they can forge outright | A slot a real visitor now cannot have |
Every row runs the wrong way for the defender, and the backlog is small by design: a few thousand slots on a tuned server, a few hundred on a default install or a small VM. That is why SYN floods survived thirty years of patching — there is no bug to fix. The attacker is using TCP exactly as specified and simply declining to finish.
Your bandwidth graph will tell you nothing
This is the part that misleads people mid-incident. A SYN packet is around 60 bytes on the wire, so the flood is measured in packets per second, not gigabits, and the traffic graph everyone looks at first stays reassuringly flat while the server refuses connections.
| SYN packets/sec | Roughly | What it typically takes down first |
|---|---|---|
| 10,000 | ~5 Mbps | An untuned backlog on a default server or small VM |
| 100,000 | ~50 Mbps | Connection-tracking tables on a stateful firewall; most single origins |
| 1,000,000 | ~0.5 Gbps | Mid-range firewalls and load balancers; anything doing per-packet state |
| 10,000,000 | ~5 Gbps | Everything that is not line-rate packet filtering upstream |
Half a gigabit is a rounding error on a modern uplink and an extinction event for a connection table. If you take one thing from this page, take that: the number that matters during a SYN flood is packets per second, and most dashboards do not show it.
Four shapes of the same attack
| Variant | What arrives at you | Why attackers use it |
|---|---|---|
| Direct SYN flood | SYN packets from the botnet's real addresses | Simplest to launch — and the only variant where blocking sources is meaningful |
| Spoofed SYN flood | SYN packets with forged source addresses | There is no attacker address to block, and your SYN-ACK replies are fired at uninvolved strangers as backscatter |
| SYN-ACK reflection | SYN-ACK packets from thousands of legitimate servers | The attacker spoofs your address toward public TCP services; their replies and retransmissions become the flood, and every source is innocent |
| ACK, RST or FIN flood | Packets matching no connection you have | Each one forces a state-table lookup, and a stateless filter cannot distinguish them from mid-connection traffic |
The last row is worth dwelling on, because it is the mirror image of the first. A SYN flood attacks the cost of starting a connection; an ACK flood attacks the cost of looking one up. Defenses tuned only for the first will pass the second straight through to the device you were protecting.
Padded SYN packets are the crossover case: the same handshake abuse with the packet inflated toward the MTU, so it exhausts state and bandwidth at once. At that point you are dealing with a volumetric attack as well, and the two have to be absorbed in different places.
Confirming that it is a SYN flood
Before you change anything, spend two minutes establishing what you are looking at:
- Count half-open connections.
ss -n state syn-recv | wc -lon Linux. Thousands of entries, climbing, is the signature.netstat -ant | grep SYN_RECVdoes the same on older systems. - Read the kernel log.
dmesgreporting "Possible SYN flooding on port 443. Sending cookies." is the kernel telling you outright, and it is also telling you the backlog already overflowed. - Check the firewall's table, not just the server's.
nf_conntrack: table full, dropping packetmeans the box in front of the application died first, which is common and easy to misread as a server fault. - Compare packet rate against bandwidth. A large packets-per-second rise with a flat bit-rate is close to diagnostic. The reverse — bandwidth pinned, packet rate ordinary — points at amplification instead.
- Look at your application log. During a SYN flood it is quiet, because requests never arrive: connections never establish. If your access log is instead full of plausible-looking requests, you are dealing with a Layer 7 attack, which is a different problem with a different answer.
That last check separates the two families faster than anything else, and getting it wrong sends people tuning WAF rules while a packet flood keeps the origin unreachable.
What you can do on the server itself
These are worth doing on any internet-facing host, today, before anything happens. They are also, on their own, insufficient — which is the point of the section after this one.
Turn on SYN cookies. net.ipv4.tcp_syncookies = 1 is the single most effective local defense. Instead of storing state for a half-open connection, the kernel encodes it into the sequence number it sends back: a hash of the four-tuple, a secret and a coarse timestamp, plus an index into a small table of MSS values. No memory is allocated. If the final ACK ever arrives, the number it acknowledges reconstructs the connection; if it never arrives, nothing was spent. The cost is real but modest — the MSS table is coarse, and TCP options such as window scaling and SACK survive only if timestamps are enabled — so cookies are engaged when the backlog overflows rather than for every connection.
Size the queues deliberately. net.ipv4.tcp_max_syn_backlog governs half-open connections; net.core.somaxconn and the backlog argument your application passes to listen() govern established ones waiting to be accepted. A generous SYN backlog in front of an application that accepts slowly just relocates the queue that overflows.
Recycle slots faster. Lowering net.ipv4.tcp_synack_retries from 5 to 2 cuts the time a dead handshake holds a slot from about 63 seconds to about 7. You lose a little tolerance for genuinely lossy clients and gain roughly nine times the slot turnover.
Do not track what you do not need to. Every SYN creates a connection-tracking entry on a stateful firewall, and nf_conntrack_max is usually the first ceiling hit. Raise it and lower the syn_recv timeout, or exempt the public listening port from tracking entirely with a notrack rule and let the kernel's own TCP stack handle handshake validation.
Rate-limit new connections per source in nftables or iptables. This helps against a direct flood from real addresses and does almost nothing against a spoofed one, because there is no stable source to limit.
Together, these raise the packet rate you survive by perhaps an order of magnitude. None of them changes the thing that decides the outcome: the packets are still arriving on your uplink and still being examined by your CPU. Every rule you add is work performed per attack packet, on hardware the attacker can out-scale for a few euros an hour. Once traffic reaches your link, the bandwidth is already spent — a point covered in more depth in what DDoS mitigation actually is.
Where a SYN flood is actually stopped
Upstream, and in a specific order — cheapest decision first, because a defense that costs more per packet than the attack does is just a slower way to lose:
- Absorption capacity in the carrier network. The flood has to land somewhere with room for it, before the last mile that leads to you.
- Stateless filtering at line rate. A forged, malformed or protocol-violating packet is droppable on sight, in the network driver, before any memory is allocated for it. This is where a flood of tens of millions of packets per second is dealt with, and it is not something a general-purpose server can do for itself.
- Handshake validation at the edge. A protection edge completes the TCP handshake on its own behalf. Only a client that answers correctly — that is, a real TCP stack at a real address — is ever proxied to your origin. A spoofed
SYNcannot complete a handshake with anyone, so the flood terminates at the edge by construction. - Per-source connection-rate caps, so one address cannot consume the handshake budget even if its packets are otherwise valid.
- Kernel-level drops for proven attackers, so a source that has already been judged costs nothing per packet thereafter.
And one prerequisite that sits outside the list, because it silently voids all of it: your origin has to be unreachable except through the protection network. If the attacker can still address packets to your server's real IP, none of the layers above are in the path. That is the most common reason protection "does not work", and the full checklist is in how to hide your origin IP address.
How Itnetic stops SYN floods
Every layer in that list is in the path on every Itnetic plan, including the free one — there is no tier where handshake protection is an upgrade.
| Layer | What it does |
|---|---|
| Upstream absorption | Frankfurt sits behind X4B, a network with 500 Gbps of mitigation capacity; Toronto and Singapore run on Linode (Akamai) behind Linode's own network-level protection. Volumetric and padded floods are soaked up before our edge, let alone yours — the current list is on the network page |
| Packet filtering at the driver | An eBPF filter at the XDP hook enforces protocol identity per flow and applies SYN caps, and drops the ACK, RST and FIN floods a stateless filter cannot recognise — all before a socket is allocated |
| Handshake termination at the edge | TCP and TLS terminate at the point of presence. Your origin only ever sees connections that completed a handshake and survived filtering, so its backlog is never the resource under attack |
| Connection-rate admission control | Per-source caps on the handshake itself, with proven visitors admitted ahead of the cap so a flood does not crowd out your regulars |
| Kernel drop of proven attackers | Repeat offenders are held in an nftables set and cost nothing per packet from then on |
| Evidence | Per-request logs with status, latency, client fingerprint and the verdict that was applied, so the post-mortem is an export rather than a reconstruction |
Two commercial details matter as much as the packet path. Scrubbed attack traffic is never metered against your bandwidth quota, so a week-long SYN flood does not arrive as an invoice. And going live is two DNS records at your existing provider — no nameserver move, about five minutes, one hostname at a time if you want to watch it first. Itnetic's DDoS protection has the rest of the mechanics.
If what you are protecting is a game server rather than a website, the handshake problem takes a different shape — join floods, ping floods and connection floods against a protocol that is not HTTP. That is covered in game server protection and in the Minecraft ping flood guide.
A six-point hardening checklist
- Enable
tcp_syncookies, and verify it withsysctl net.ipv4.tcp_syncookiesrather than assuming your distribution did it. - Set
tcp_max_syn_backlog,somaxconnand your application'slisten()backlog to consistent values, largest first. - Lower
tcp_synack_retriesto 2 so dead handshakes release their slots in seconds rather than a minute. - Check
nf_conntrack_maxagainst your real connection rate, or stop tracking the public listening port. - Put per-second packet-rate on a dashboard next to bandwidth, and alert on it. You cannot respond to the metric you do not plot.
- Firewall the origin so it accepts traffic only from your protection network — otherwise everything upstream is optional from the attacker's point of view.
If you are in the middle of one right now, stop reading and work through how to stop a DDoS attack; it is written for the incident rather than for the reading list. When the wave has passed, how to test your DDoS protection covers how to confirm — safely and legally — that the next one lands somewhere other than your connection table.