Feature Request: limit_req support in the stream module (currently http-only)

Please use this template for troubleshooting questions.

My issue: I’m looking for a way to rate-limit connection attempts per second from a single source IP on a stream (TCP/TLS) listener — equivalent to what limit_req does for http. The stream module only offers limit_conn, which caps concurrent connections, not the rate of new attempts over time. Because of this, a client can perform repeated failed TLS handshakes (e.g., invalid/missing client certificate) in rapid succession without ever tripping limit_conn, since each failed handshake closes quickly and frees the slot immediately. Each attempt still costs real CPU (TCP accept + TLS negotiation + client-cert chain verification) before being rejected — a low-and-slow handshake-flood vector that neither limit_conn nor ssl_handshake_timeout mitigates.

Is there any existing way (including NGINX Plus) to achieve this, and if not, is bringing limit_req-style rate limiting to the stream module on any roadmap?

How I encountered the problem: We proxy a TLS-terminated, mutually-authenticated TCP protocol (MQTT) through the stream module with ssl_verify_client on. While reviewing our per-source-IP abuse protections, I noticed our http/WebSocket listener’s limit_req correctly throttles all attempts per IP regardless of success/failure, but the equivalent protection isn’t possible on the stream listener since limit_req doesn’t exist there.

Solutions I’ve tried:
Confirmed ngx_stream_limit_req_module does not exist in nginx (checked official docs and changelog — only ngx_stream_limit_conn_module is available for stream).

  • Currently using limit_conn (concurrency cap) plus ssl_handshake_timeout 10s to bound per-handshake cost, but this doesn’t limit attempt frequency.

Version of NGINX or NGINX adjacent software (e.g. NGINX Gateway Fabric): nginx 1.31.2

Deployment environment: Docker container (Debian/trixie based image), nginx as a TLS-terminating TCP proxy in front of an MQTT broker.

Minimal NGINX config to reproduce your issue (preferably running on NGINX Playground | tech-playground.com for ease of debugging, and if not as a code block): (Tip → Run nginx -T to print your entire NGINX config to your terminal.)

NGINX access/error log: (Tip → You can usually find the logs in the /var/log/nginx directory.)

You’re right that there’s no limit_req equivalent in the stream module today —limit_conn only caps concurrency, and as you’ve found, fast-closing failed handshakes never occupy a slot long enough to trip it.

For per-source connection-rate limiting in front of a TCP/TLS listener, the canonical answer is to do it at the packet-filter layer rather than in nginx. That’s not a workaround so much as the right tool: it drops the offending SYNs in the kernel before nginx accepts the connection, so you pay nothing for the TCP accept, TLS negotiation or client-cert verification — the exact costs you’re trying to avoid.

nftables (modern; rate-limit new connections per source IP to your listener port, e.g. 8883):

# drop new connections from a source exceeding 10/s (burst 20) to 8883

tcp dport 8883 ct state new \
meter mqtt-rate { ip saddr limit rate over 10/second burst 20 packets } \
drop

iptables (legacy) equivalent with hashlimit:

iptables -A INPUT -p tcp --dport 8883 --syn -m conntrack --ctstate NEW \
-m hashlimit --hashlimit-name mqtt-rate --hashlimit-mode srcip \
--hashlimit-above 10/second --hashlimit-burst 20 -j DROP

Tune the rate/burst to your legitimate client behaviour. You can keep your existing limit_conn and ssl_handshake_timeout as complementary concurrency/duration bounds.

If you specifically want to react to failed handshakes (e.g. repeated invalid client certs), pairing this with fail2ban watching the stream error log for client SSL certificate verify error lines — and banning at the firewall — gives you an adaptive, longer-duration block on top of the steady-state rate cap.

A native ngx_stream_limit_req_module would be a reasonable enhancement and isn’t unfeasible, but nothing like it exists in nginx (open source or Plus) at present, and for a pure handshake-flood defence the firewall approach is both simpler and cheaper.