From 2433b18fefbabcf5552b3bba91fd17824b335379 Mon Sep 17 00:00:00 2001
From: Kangmin Kim <76634341+amazon7737@users.noreply.github.com>
Date: Wed, 29 Apr 2026 23:22:06 +0900
Subject: [PATCH] Add limit-connections support
---
.../kubernetes/ingress-nginx.md | 2 +-
.../kubernetes/ingress-nginx/annotations.go | 1 +
.../ingress-with-limit-connections.yml | 20 ++++
.../ingress-nginx/kubernetes_test.go | 99 +++++++++++++++++++
.../kubernetes/ingress-nginx/middleware.go | 15 +++
.../kubernetes/ingress-nginx/model.go | 3 +
.../kubernetes/ingress-nginx/translator.go | 6 ++
7 files changed, 145 insertions(+), 1 deletion(-)
create mode 100644 pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-limit-connections.yml
diff --git a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md
index 5f06356f11..2739305088 100644
--- a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md
+++ b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md
@@ -376,6 +376,7 @@ The following annotations are organized by category for easier navigation.
| `nginx.ingress.kubernetes.io/limit-rps` | Exceeding the limit returns `429 Too Many Requests` instead of NGINX's default `503 Service Unavailable`. |
| `nginx.ingress.kubernetes.io/limit-rpm` | Exceeding the limit returns `429 Too Many Requests` instead of NGINX's default `503 Service Unavailable`. |
| `nginx.ingress.kubernetes.io/limit-burst-multiplier` | Default to a multiplier of 5 if the configured value is less than 1. Exceeding the limit returns `429 Too Many Requests` instead of NGINX's default `503 Service Unavailable`. |
+| `nginx.ingress.kubernetes.io/limit-connections` | Exceeding the limit returns `429 Too Many Requests` instead of NGINX's default `503 Service Unavailable`. The concurrent connection limit is evaluated per client IP address. Values less than or equal to `0` are safely ignored. |
### Buffering
@@ -454,7 +455,6 @@ In practice, Traefik is slightly more lenient under bursty load, as it smooths o
| `nginx.ingress.kubernetes.io/limit-rate-after` | |
| `nginx.ingress.kubernetes.io/limit-rate` | |
| `nginx.ingress.kubernetes.io/limit-whitelist` | |
-| `nginx.ingress.kubernetes.io/limit-connections` | |
| `nginx.ingress.kubernetes.io/global-rate-limit` | |
| `nginx.ingress.kubernetes.io/global-rate-limit-window` | |
| `nginx.ingress.kubernetes.io/global-rate-limit-key` | |
diff --git a/pkg/provider/kubernetes/ingress-nginx/annotations.go b/pkg/provider/kubernetes/ingress-nginx/annotations.go
index d2d8f58ee2..3b8319c0bc 100644
--- a/pkg/provider/kubernetes/ingress-nginx/annotations.go
+++ b/pkg/provider/kubernetes/ingress-nginx/annotations.go
@@ -89,6 +89,7 @@ type IngressConfig struct {
LimitRPM *int `annotation:"nginx.ingress.kubernetes.io/limit-rpm"`
LimitRPS *int `annotation:"nginx.ingress.kubernetes.io/limit-rps"`
LimitBurstMultiplier *int `annotation:"nginx.ingress.kubernetes.io/limit-burst-multiplier"`
+ LimitConnections *int `annotation:"nginx.ingress.kubernetes.io/limit-connections"`
CustomHeaders *string `annotation:"nginx.ingress.kubernetes.io/custom-headers"`
UpstreamVHost *string `annotation:"nginx.ingress.kubernetes.io/upstream-vhost"`
diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-limit-connections.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-limit-connections.yml
new file mode 100644
index 0000000000..1ac9eb7493
--- /dev/null
+++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/ingress-with-limit-connections.yml
@@ -0,0 +1,20 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-with-limit-connections
+ namespace: default
+ annotations:
+ nginx.ingress.kubernetes.io/limit-connections: "10"
+spec:
+ ingressClassName: nginx
+ rules:
+ - host: whoami.localhost
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: whoami
+ port:
+ number: 80
diff --git a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go
index ea3e78156e..1839fca436 100644
--- a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go
+++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go
@@ -14232,6 +14232,105 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
+ {
+ desc: "Limit connections",
+ paths: []string{
+ "services.yml",
+ "ingressclasses.yml",
+ "ingresses/ingress-with-limit-connections.yml",
+ },
+ expected: &dynamic.Configuration{
+ TCP: &dynamic.TCPConfiguration{
+ Routers: map[string]*dynamic.TCPRouter{},
+ Services: map[string]*dynamic.TCPService{},
+ },
+ HTTP: &dynamic.HTTPConfiguration{
+ Routers: map[string]*dynamic.Router{
+ "default-ingress-with-limit-connections-rule-0-path-0": {
+ EntryPoints: []string{"http"},
+ Rule: `Host("whoami.localhost") && PathPrefix("/")`,
+ RuleSyntax: "default",
+ Middlewares: []string{"default-ingress-with-limit-connections-rule-0-path-0-limit-connections", "default-ingress-with-limit-connections-rule-0-path-0-retry"},
+ Service: "default-ingress-with-limit-connections-whoami-80",
+ Observability: &dynamic.RouterObservabilityConfig{
+ Metadata: &dynamic.ObservabilityMetadata{
+ Ingress: &dynamic.KubernetesIngressMetadata{
+ Namespace: "default",
+ IngressName: "ingress-with-limit-connections",
+ ServiceName: "whoami",
+ ServicePort: "80",
+ },
+ },
+ },
+ },
+ "default-ingress-with-limit-connections-rule-0-path-0-tls": {
+ EntryPoints: []string{"https"},
+ Rule: `Host("whoami.localhost") && PathPrefix("/")`,
+ RuleSyntax: "default",
+ Middlewares: []string{"default-ingress-with-limit-connections-rule-0-path-0-tls-limit-connections", "default-ingress-with-limit-connections-rule-0-path-0-tls-retry"},
+ Service: "default-ingress-with-limit-connections-whoami-80",
+ TLS: &dynamic.RouterTLSConfig{},
+ Observability: &dynamic.RouterObservabilityConfig{
+ Metadata: &dynamic.ObservabilityMetadata{
+ Ingress: &dynamic.KubernetesIngressMetadata{
+ Namespace: "default",
+ IngressName: "ingress-with-limit-connections",
+ ServiceName: "whoami",
+ ServicePort: "80",
+ },
+ },
+ },
+ },
+ },
+ Middlewares: map[string]*dynamic.Middleware{
+ "default-ingress-with-limit-connections-rule-0-path-0-retry": {Retry: &dynamic.Retry{Attempts: 3}},
+ "default-ingress-with-limit-connections-rule-0-path-0-tls-retry": {Retry: &dynamic.Retry{Attempts: 3}},
+ "default-ingress-with-limit-connections-rule-0-path-0-limit-connections": {
+ InFlightReq: &dynamic.InFlightReq{
+ Amount: 10,
+ SourceCriterion: &dynamic.SourceCriterion{
+ IPStrategy: &dynamic.IPStrategy{},
+ },
+ },
+ },
+ "default-ingress-with-limit-connections-rule-0-path-0-tls-limit-connections": {
+ InFlightReq: &dynamic.InFlightReq{
+ Amount: 10,
+ SourceCriterion: &dynamic.SourceCriterion{
+ IPStrategy: &dynamic.IPStrategy{},
+ },
+ },
+ },
+ },
+ Services: map[string]*dynamic.Service{
+ "unavailable-service": {
+ LoadBalancer: &dynamic.ServersLoadBalancer{
+ Strategy: "wrr",
+ PassHostHeader: ptr.To(true),
+ ResponseForwarding: &dynamic.ResponseForwarding{
+ FlushInterval: dynamic.DefaultFlushInterval,
+ },
+ },
+ },
+ "default-ingress-with-limit-connections-whoami-80": {
+ LoadBalancer: &dynamic.ServersLoadBalancer{
+ Servers: []dynamic.Server{{URL: "http://10.10.0.1:80"}, {URL: "http://10.10.0.2:80"}},
+ Strategy: "wrr",
+ PassHostHeader: ptr.To(true),
+ ResponseForwarding: &dynamic.ResponseForwarding{FlushInterval: dynamic.DefaultFlushInterval},
+ ServersTransport: "default-ingress-with-limit-connections",
+ },
+ },
+ },
+ ServersTransports: map[string]*dynamic.ServersTransport{
+ "default-ingress-with-limit-connections": {
+ ForwardingTimeouts: &dynamic.ForwardingTimeouts{DialTimeout: ptypes.Duration(60 * time.Second), ReadTimeout: ptypes.Duration(60 * time.Second), WriteTimeout: ptypes.Duration(60 * time.Second), IdleConnTimeout: ptypes.Duration(60 * time.Second)},
+ },
+ },
+ },
+ TLS: &dynamic.TLSConfiguration{},
+ },
+ },
{
desc: "Use Regex with Prefix pathType and StrictValidatePathType enabled",
paths: []string{
diff --git a/pkg/provider/kubernetes/ingress-nginx/middleware.go b/pkg/provider/kubernetes/ingress-nginx/middleware.go
index 1f49d5d6d0..cb38a5cb1f 100644
--- a/pkg/provider/kubernetes/ingress-nginx/middleware.go
+++ b/pkg/provider/kubernetes/ingress-nginx/middleware.go
@@ -34,6 +34,7 @@ func (p *Provider) buildMiddlewares(ctx context.Context, loc *location, hostname
p.buildRewriteTarget(loc)
p.buildUpstreamVhost(loc)
p.buildRateLimit(loc)
+ p.buildLimitConnections(loc)
p.buildAuthTLSPassCert(loc)
p.buildCustomHeaders(loc)
p.buildSnippetAuth(loc)
@@ -310,6 +311,20 @@ func (p *Provider) buildRateLimit(loc *location) {
}
}
+func (p *Provider) buildLimitConnections(loc *location) {
+ limit := ptr.Deref(loc.Config.LimitConnections, 0)
+ if limit <= 0 {
+ return
+ }
+
+ loc.LimitConnections = &dynamic.InFlightReq{
+ Amount: int64(limit),
+ SourceCriterion: &dynamic.SourceCriterion{
+ IPStrategy: &dynamic.IPStrategy{},
+ },
+ }
+}
+
func (p *Provider) buildAuthTLSPassCert(loc *location) {
if !ptr.Deref(loc.Config.AuthTLSPassCertificateToUpstream, false) || loc.Config.AuthTLSSecret == nil {
return
diff --git a/pkg/provider/kubernetes/ingress-nginx/model.go b/pkg/provider/kubernetes/ingress-nginx/model.go
index 6a5031216a..74dd701fc2 100644
--- a/pkg/provider/kubernetes/ingress-nginx/model.go
+++ b/pkg/provider/kubernetes/ingress-nginx/model.go
@@ -193,6 +193,9 @@ type location struct {
// RateLimitRPS, if non-nil, applies a per-second request rate limit.
RateLimitRPS *dynamic.RateLimit
+ // LimitConnections, if non-nil, caps concurrent in-flight requests per source IP.
+ LimitConnections *dynamic.InFlightReq
+
// AuthTLSPassCert, if non-nil, forwards the client TLS certificate to the backend.
AuthTLSPassCert *dynamic.AuthTLSPassCertificateToUpstream
diff --git a/pkg/provider/kubernetes/ingress-nginx/translator.go b/pkg/provider/kubernetes/ingress-nginx/translator.go
index d68b01901c..cd3e7b6de8 100644
--- a/pkg/provider/kubernetes/ingress-nginx/translator.go
+++ b/pkg/provider/kubernetes/ingress-nginx/translator.go
@@ -501,6 +501,12 @@ func (p *Provider) applyMiddlewares(mc *model, loc *location, routerKey string,
rt.Middlewares = append(rt.Middlewares, name)
}
+ if loc.LimitConnections != nil {
+ name := routerKey + "-limit-connections"
+ conf.HTTP.Middlewares[name] = &dynamic.Middleware{InFlightReq: loc.LimitConnections}
+ rt.Middlewares = append(rt.Middlewares, name)
+ }
+
if loc.AuthTLSPassCert != nil && rt.TLS != nil {
name := routerKey + "-pass-certificate-to-upstream"
conf.HTTP.Middlewares[name] = &dynamic.Middleware{AuthTLSPassCertificateToUpstream: loc.AuthTLSPassCert}