A TCP connection flood never sends any data. It opens connections, completes the handshake, then says nothing for as long as the socket is allowed to live. There is no request to inspect, no payload to filter and no signature to match, because nothing above the transport layer ever happens.
That makes it awkward to defend against and easy to miss. Bandwidth stays flat and requests per second stay flat. What fills up is a set of counters most teams have never put on a dashboard: connection-table entries, file descriptors, connection-tracking slots and the queue of connections waiting to be accepted. The service stops answering while every graph anyone is watching reads normal.
The short version
- Nothing is ever sent. No HTTP, no TLS, no payload, no negotiation beyond the handshake. An idle socket produces no traffic, so there is nothing to inspect and nothing for a volume-based defense to trigger on.
- It exhausts counters, not capacity. Connection-table entries, file descriptors, connection-tracking slots, per-source socket limits and the accept queue. None appear on a bandwidth or request-rate graph.
- A WAF is not a firewall, and here that difference decides the outcome. A WAF inspects HTTP and cannot see this traffic at all. A network firewall works at the layer that can.
- It is the cheapest attack per socket we run. No crypto cost on either side, unlike a TLS flood.
- A single-source test will tell you that you are fine. A per-IP connection cap stops one machine in seconds and does little against a distributed flood, so the cheap check passes where the real risk lives.
This is one post in a series covering each of the ten simulation types Obsidio runs. They fail in genuinely different places, which is why a test programme needs coverage across the categories rather than volume in one of them. For how they fit together, start with our guide on how to test your DDoS protection.
The ten simulation types
One post each, grouped the way the platform groups them.
Application-layer floods
Slow attacks
Connection exhaustion
- TLS Flood
- TCP Connection Floodyou are here
Protocol-specific and scripted
- DNS Floodcoming soon
- Puppeteer Scriptfunctional load, not an attack
What a TCP connection flood actually does
Each parallel slot opens one raw TCP connection, lets the handshake complete, and holds the socket idle until a configured timeout expires. Then it closes and opens a fresh one. There is no HTTP layer and no TLS layer above it, and no application data is written in either direction. A TCP flood attack of this kind is about occupancy rather than packet volume, and the connections it holds are fully established rather than half-open.
What that consumes is a set of finite slots. Every established connection occupies an entry in the server’s connection table, an open file descriptor in the process holding it, a connection-tracking entry in every stateful device on the path, and a share of whatever per-source socket limit applies. Connections arriving while the accept queue is backed up wait in it, and once it is full they are refused. None of those are CPU or bandwidth, which is why the attack is so quiet.
Twenty people each phone the shop and say nothing. Every line is occupied, the staff are sitting there ready to help, and real customers get a busy signal.
A socket that has connected and not yet spoken is not suspicious on its own. A mobile client on a bad connection looks the same. That is why servers do not simply hang up, and why the defense has to be a limit rather than a rule.
Which of your controls should have caught this
It is worth walking the whole chain a connection passes through, because the answer here is narrower than most teams expect. What sits in front, what absorbs whatever gets through, and whether anything tells you it happened.
Zone 1
Should any of these have stopped it?
- Web application firewall. Inspects HTTP requests. No request is ever made, so it has nothing to evaluate.
- Network firewall. The one control at the right layer. Whether it caps connections per source, and whether its own tracking table has headroom, is a question about your configuration.
- Volumetric DDoS protection. Watches bandwidth and packet rate. An idle socket generates neither.
- Rate limiting and bot detection. One counts requests, the other profiles client behaviour. There is neither.
- A proxy or scrubbing layer that terminates TCP for you. Real protection where it exists, provided your origin cannot be reached directly.
Zone 2
What takes the hit?
- Load balancer or reverse proxy. If it terminates connections, its own limits decide the outcome. Confirm which component holds the socket.
- Connection tracking and the accept queue. One entry per socket, and new arrivals queue behind everything already held.
- The listening service. Where it bites. The per-worker connection ceiling and the open-file limit are hard walls, and the lower one is what you hit.
- Application and database. Never reached. No request is made, so no handler runs and no query is issued.
- Auto-scaling. CPU, bandwidth and request rate all look idle, so nothing scales. More nodes would not help a shared stateful device anyway.
Zone 3
Would you find out?
- Bandwidth and request-rate dashboards. Flat throughout. Both are measuring the wrong thing.
- A concurrent-connections or tracking-table graph. The one internal metric that shows this clearly. Whether it exists, and whether anyone watches it, is a real question.
- A check from outside your network. Catches it reliably, because new connections stop being accepted. Only useful if you run one.
- Access logs and your SIEM. No request means no access-log line. Anything recorded lands in an error log, under a resource-limit message rather than an attack.
- Alerting and on-call. Nothing crosses a threshold. The service is refusing connections and the room is quiet.
Purple marks what only a test on your own infrastructure can settle. Everything else follows from how the attack works, not from how well your team configured things.
The firewall and the WAF are worth separating properly, because this is the attack where treating them as one box gets people badly wrong. A web application firewall reads HTTP: request lines, headers, query strings, bodies. A connection that completes the handshake and then stays silent gives it none of that, and a client which has not spoken yet is not an anomaly a rule can be written against. The WAF is not misconfigured. The traffic is below the layer it works at.
A network firewall works with addresses, ports and connection state, which is what this attack manipulates, so it can count how many established connections one source holds and refuse the next. In netfilter that is the connlimit match, where --connlimit-above sets the threshold and --connlimit-mask groups sources by prefix length; nftables has ct count. It pays for being in the game, though: every connection it tracks costs an entry in its own table, so the control best placed to see the attack is also a resource the attack consumes.
The parameters, and what this is not
Four settings, each mapping to a resource on your side:
- Sockets held per worker. Default 20, range 1 to 100. Multiplied by the worker count, this is how many entries you are asking the target’s connection table to hold. The real ceiling is the smaller of that product and the target’s per-source and global socket limits.
- Idle timeout. Default 600,000 milliseconds, or ten minutes, settable from 100 milliseconds to an hour and capped at the run duration. Higher holds sockets longer, so pressure is sustained and the open rate falls. Lower churns faster, shifting load to accept and teardown work instead of occupancy.
- Think time. Default 0. How long a slot waits after a socket closes before opening the next, which paces the replacement rate.
- Port. Defaults to 443 or 80 from the target URL scheme. The scheme only picks the number; the connection is plain TCP either way.
A TLS flood also abandons connections, but it makes the server complete a full handshake first, including key exchange, certificate and session setup, and that asymmetric crypto costs the server roughly fifteen times what it costs the client. The target is CPU. A TCP connection flood spends nothing on crypto at either end, and its cost to the target is one table entry and one descriptor.
Slowloris is different again. It speaks HTTP, sending a partial request and then trickling, which is why header-read timeouts and a buffering proxy defeat it. A TCP connection flood sends no HTTP at all, so anything reasoning about request content is out of scope here, not merely weak.
How to defend against a TCP connection flood
Everything useful here is a number, and every number can be stale, wrong, or lower than you assume. These are the ones to check, with documented defaults where they exist:
- Per-source concurrent connection limits, at the network layer. netfilter’s
connlimitmatch with--connlimit-aboveis the direct tool, and--connlimit-maskgroups a source network rather than one address. One warning: nginx’slimit_conndoes not cover this case. Its documentation is explicit that “a connection is counted only if it has a request being processed by the server and the whole request header has already been read”. A socket that never sends a request is never counted. - Connection-tracking capacity. In the Linux connection tracker,
nf_conntrack_maxdefaults to four times the bucket count, and on a machine with more than 4 GB of memory the bucket count defaults to 65,536. More importantly,nf_conntrack_tcp_timeout_establisheddefaults to 432,000 seconds, which is five days. An idle established connection keeps its entry for days unless something else closes the socket. - File-descriptor limits. Every socket is an open file.
fs.file-maxcaps the handles the kernel will allocate system-wide, with no documented default because it is sized per system. Per-process limits usually bind first: nginx setsworker_connectionsto 512 per worker, counts proxied connections as well as client ones, and notes the count “cannot exceed the current limit on the maximum number of open files”, raised withworker_rlimit_nofile, which has no default. Trust the smaller number. - Accept-queue depth. nginx sets the listen backlog to 511 on Linux by default, and whatever a service asks for is silently capped to
net.core.somaxconn, which defaults to 4096 since Linux 5.4 and to 128 on earlier kernels. A short queue turns held connections into refused ones sooner. - Idle timeouts, at a layer that has one. The transport layer will not help by default:
tcp_keepalive_timedefaults to two hours, then nine probes at 75-second intervals. nginx does better, withclient_header_timeoutat 60 seconds for reading a request header andkeepalive_timeoutat 75. Whether every listener in your stack has an equivalent is the thing to verify, and plenty do not.
A listener with a 60-second header timeout will close these sockets, since the simulation holds them for ten minutes by default. That moves the interesting question rather than answering it. What happens in front of the listener, in the firewall’s table, the load balancer and the accept queue, is where the run gets decided.
What you can check yourself, and what it will get wrong
Two of these you can do this afternoon. The third is the one that misleads.
Read the numbers and do the arithmetic. Pull the connection-tracking maximum, the descriptor limits, the per-worker connection ceiling, the listen backlog and somaxconn. The smallest is your real capacity. This is free, takes an hour, and often surprises people. It tells you where the ceiling is, not what happens when you reach it.
Test alert routing on its own. Fire a synthetic alert and see whether it reaches the right rotation and whether anybody acts on it. That needs no attack traffic.
Open a lot of connections from one machine against staging infrastructure you own, with change-management sign-off. Legitimate, and this is where the trap is.
The arithmetic is worth spelling out. Say your per-source cap is 100 concurrent connections. One machine tries to open five thousand and is stopped at 100. Refusals appear in the firewall counters, the connection count barely moves, the service stays healthy, and the test is written up as a pass. The result is accurate. The conclusion drawn from it is wrong, because the control that just proved itself is close to the least relevant one for the scenario you are afraid of.
Now distribute it. Ten thousand sources each hold twenty connections. Every source sits far below the cap, so not one breaks a rule, and your table is carrying two hundred thousand established connections. Same firewall, same configuration, opposite outcome. A per-source cap does not reduce the total capacity that can be consumed; it limits any one source’s share. Against enough sources the binding constraint moves to your tracking table and descriptor ceiling, the numbers the single-source test never came close to because the cap stopped it at 100 first. Prefix masking has the same problem: grouping by network catches a rented block at one hosting provider and does little against residential addresses spread across thousands of unrelated networks.
Once the cap looks like the defense, nobody raises the underlying limits and nobody graphs the tracking table, so the finding that mattered is never generated. And no configuration review tells you how the stack behaves at its ceiling. Some services refuse new connections cleanly, some stall, some fail their health check and get pulled from rotation while still running.
How Obsidio tests a TCP connection flood
Obsidio runs this as a controlled, authorized simulation against infrastructure you own. Domain ownership is verified by DNS TXT record before any traffic flows, runs ramp up gradually rather than starting at full load, and any run can be aborted live. You choose the sockets per worker, the idle timeout, the think time and the port.
Load comes from 100,000+ globally distributed real devices rather than a handful of datacenter instances. That is the part a self-test cannot reproduce: a per-source cap gets exercised the way a distributed flood would exercise it, across many networks, each source individually unremarkable.
- Ignore the request-rate figure. The displayed rate counts socket opens, not throughput, and the per-slot estimate is one. It looks trivially low while the run is succeeding. Watch concurrent connections and the peak instead, plus the open-attempt rate if you are testing churn rather than occupancy.
- Measure reachability from outside. What users experience is a connection that is not accepted, and server-side metrics can hide that.
Be clear about the split before you start. Obsidio measures how many connections were held, the peak, the open rate and whether the service stayed reachable from outside, because that is visible externally. Whether your descriptor ceiling or tracking table actually saturated, what your error log recorded and whether anyone was paged live only on your own consoles. Have someone watching them during the window.
Strength is proven, not promised. A TCP connection flood does not test your bandwidth or your rules. It tests whether the limits your stack was sized with are still the limits it is running.
Where this fits in a testing programme
TCP Connection Flood belongs in the connection-exhaustion category alongside TLS Flood. Both aim at the connection rather than the request: one buys CPU time with the server’s own cryptography, the other buys table space with silence. A stack that absorbs one can fail the other, which is why the category needs both.
Across the ten simulation types the failure point moves considerably. An application-layer flood gives your front defenses something to act on, reaches as far as your database connection pool, and turns the detection problem from silence into noise. This one stops at the transport layer, is invisible to your WAF, and is decided by five or six integers in configuration files.
For regulated institutions the run also has to leave a record. Obsidio produces cryptographically attested, tamper-evident reports mapped to FINMA, DORA and NIS2, generated inside Trusted Execution Environments, so the outcome is an artifact you can file rather than a screenshot you vouch for.
To scope an authorized TCP connection flood simulation against your own infrastructure, see the Obsidio platform or get in touch with the team. Independent, neutral, verifiable: resilience you can prove.
