nftables can limit the number of connections an IP address can establish to your server, which is a really effective way to mitigate basic DoS attacks or prevent runaway client applications from hogging resources.

Let’s see it in action. Imagine we have a web server running on port 80 and we want to limit each IP to a maximum of 10 concurrent connections.

# First, create a new table if you don't have one for your rules
nft add table ip my_web_firewall

# Then, create a chain for incoming connections to the web server
nft add chain ip my_web_firewall input { type filter hook input priority 0 \; }

# Now, add the rule to limit connections.
# We're targeting TCP traffic on port 80.
# 'connlimit' is the key primitive here.
# 'connlimit-above 10' means "if the number of existing connections from this source IP is GREATER THAN 10..."
# 'reject' is used to drop the incoming packet and send a TCP RST back to the client.
nft add rule ip my_web_firewall input tcp dport 80 ct state new connlimit-above 10 reject with tcp reset

Here’s what’s happening under the hood. nftables uses the conntrack subsystem to keep track of active connections. When a new connection attempt (a SYN packet, which ct state new matches) arrives for TCP port 80, nftables consults conntrack to count how many established connections already exist from that specific source IP address. If that count is already 10 or more, the connlimit-above 10 condition is met, and the packet is rejected.

The ct state new part is crucial. We only want to enforce the limit when a new connection is being established. Existing, ongoing connections shouldn’t be affected by this rule. If we omitted ct state new, the rule would also trigger on subsequent packets from an already established connection, potentially breaking legitimate traffic.

The reject with tcp reset action is a polite way of saying "no." It tells the client that the connection is being refused, which is generally better than just dropping the packet silently (which would be drop). A silent drop can cause clients to retry indefinitely, consuming more resources on their end and potentially looking like a different kind of network issue.

You can also specify the direction of the limit. By default, connlimit counts connections from the source IP to the destination IP and port. If you wanted to limit how many connections your server can initiate to a specific external IP (less common but possible), you’d use connlimit-mask and specify the destination IP. However, for mitigating incoming abuse, the default is what you want.

The connlimit module also accepts a connlimit-upto option. connlimit-upto 10 would mean "if the number of existing connections is LESS THAN OR EQUAL TO 10, accept it, otherwise drop it." This is useful if you want to allow exactly N connections and nothing more, but connlimit-above is more typical for rate-limiting and DoS prevention where you want to stop excess connections.

It’s worth noting that the connlimit primitive operates on the source IP address by default. If you have multiple servers behind a NAT gateway and want to limit connections per internal IP, this simple rule won’t work as expected because all external traffic will appear to come from the NAT gateway’s IP. In such scenarios, you’d need more advanced nftables configurations, potentially involving ip daddr in the connlimit statement or using nftables sets to track individual internal IPs if your NAT setup allows it.

The next thing you’ll likely encounter when hardening your firewall is the need to limit connection rates rather than just connection counts, which is where modules like rate come into play.

Want structured learning?

Take the full Nftables course →