Repository: traefik/traefik (language: go)

Issue:
Support for Redis in RateLimiter middleware
### Do you want to request a *feature* or report a *bug*?

Feature

### What did you expect to see?

Currently, all rate limiter data is kept in the memory which is a problem when you scale to more instances of Traefik. Many different projects are using our APIs (  custom header with project name as source criterion ). The only way we get it to work is to configure our load balancer to always send traffic for a specific project to the same instance of Traefik, which has its issues.  We would like to use Redis to keep all this data in one place.

Also, if there is no reason why it shouldn't be implemented, I would like to contribute to the project.


Discussion / hints:
Hi,
We're wary of what you're proposing because among other things, we suspect it might entail more complexity than what you may have anticipated. And we do not want to maintain such a complexity just for the rate limiting middleware.
So could you please elaborate a bit more on the kind of solution you would envision?
I would probably use the package [Throttled](https://github.com/throttled/throttled). We tested it and it looks solid and is well maintained. The only thing it needs is some key, which will be generated based on the sourceCriterion type and value (IP, header value...).

Also, it probably should be a new middleware, because it needs different settings, like RedisRateLimiter or something more general (package supports more store implementations).

The next problem is that Redis client and rate limiter support many options, I'm not sure whether to set some hardcoded values or give user a way to configure everything.
@samuelbednarcik thanks.
We've labeled this as a proposal because we don't want to dismiss it without thinking more about it, but on the other hand we don't really know when we'll be able to dedicate more time to it.
So feel free to investigate it on your end, but please keep in mind that until further notice we probably would not be able to spend time on reviewing a PR related to that proposal.
This is a good proposal, redis will not only allow scaling but also maintaining a log of rate limiting over a longer period of time (say rate limiting 1000reqs/week) since there will be some level of persistence.

Of course this functionality can be delegated to the internal web framework, but a redis store can open up ideas for more middlewares like dynamic or precision IP filtering (banning thousands of individual IP's).
This will allow shared rate limit across Traefik replicas, since Traefik is a stateless application, each replicas rely on its own variables

Fix patch:
diff --git a/docs/content/middlewares/http/ratelimit.md b/docs/content/middlewares/http/ratelimit.md
index dc03dc9a37..8759e9255e 100644
--- a/docs/content/middlewares/http/ratelimit.md
+++ b/docs/content/middlewares/http/ratelimit.md
@@ -496,3 +496,718 @@ http:
     [http.middlewares.test-ratelimit.rateLimit.sourceCriterion]
       requestHost = true
 ```
+
+### `redis`
+
+Enables distributed rate limit using `redis` to store the tokens.
+If not set, Traefik's in-memory storage is used by default.
+
+#### `redis.endpoints`
+
+_Required, Default="127.0.0.1:6379"_
+
+Defines how to connect to the Redis server.
+
+```yaml tab="Docker & Swarm"
+labels:
+  - "traefik.http.middlewares.test-ratelimit.ratelimit.redis.endpoints=127.0.0.1:6379"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: test-ratelimit
+spec:
+  rateLimit:
+    # ...
+    redis:
+      endpoints:
+        - "127.0.0.1:6379"
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.endpoints=127.0.0.1:6379"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-ratelimit:
+      rateLimit:
+        # ...
+        redis:
+          endpoints:
+            - "127.0.0.1:6379"
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-ratelimit.rateLimit]
+    [http.middlewares.test-ratelimit.rateLimit.redis]
+      endpoints = ["127.0.0.1:6379"]
+```
+
+#### `redis.username`
+
+_Optional, Default=""_
+
+Defines the username used to authenticate with the Redis server.
+
+```yaml tab="Docker & Swarm"
+labels:
+    - "traefik.http.middlewares.test-ratelimit.ratelimit.redis.username=user"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+   name: test-ratelimit
+spec:
+   rateLimit:
+      # ...
+      redis:
+         secret: mysecret
+
+---
+apiVersion: v1
+kind: Secret
+metadata:
+   name: mysecret
+   namespace: default
+
+data:
+   username: dXNlcm5hbWU=
+   password: cGFzc3dvcmQ=
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.username=user"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-ratelimit:
+      rateLimit:
+        # ...
+        redis:
+          username: user
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-ratelimit.rateLimit]
+    [http.middlewares.test-ratelimit.rateLimit.redis]
+      username = "user"
+```
+
+#### `redis.password`
+
+_Optional, Default=""_
+
+Defines the password to authenticate against the Redis server.
+
+```yaml tab="Docker & Swarm"
+labels:
+    - "traefik.http.middlewares.test-ratelimit.ratelimit.redis.password=password"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+   name: test-ratelimit
+spec:
+   rateLimit:
+      # ...
+      redis:
+         secret: mysecret
+
+---
+apiVersion: v1
+kind: Secret
+metadata:
+   name: mysecret
+   namespace: default
+
+data:
+   username: dXNlcm5hbWU=
+   password: cGFzc3dvcmQ=
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.password=password"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-ratelimit:
+      rateLimit:
+        # ...
+        redis:
+          password: password
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-ratelimit.rateLimit]
+    [http.middlewares.test-ratelimit.rateLimit.redis]
+      password = "password"
+```
+
+#### `redis.db`
+
+_Optional, Default=0_
+
+Defines the database to select after connecting to the Redis.
+
+```yaml tab="Docker & Swarm"
+labels:
+  - "traefik.http.middlewares.test-ratelimit.ratelimit.redis.db=0"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: test-ratelimit
+spec:
+  rateLimit:
+    # ...
+    redis:
+      db: 0
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.db=0"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-ratelimit:
+      rateLimit:
+        # ...
+        redis:
+          db: 0
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-ratelimit.rateLimit]
+    [http.middlewares.test-ratelimit.rateLimit.redis]
+      db = 0
+```
+
+#### `redis.tls`
+
+Same as this [config](https://doc.traefik.io/traefik/providers/redis/#tls)
+
+_Optional_
+
+Defines the TLS configuration used for the secure connection to Redis.
+
+##### `redis.tls.ca`
+
+_Optional_
+
+`ca` is the path to the certificate authority used for the secure connection to Redis,
+it defaults to the system bundle.
+
+```yaml tab="Docker & Swarm"
+labels:
+    - "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.ca=path/to/ca.crt"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: test-ratelimit
+spec:
+  rateLimit:
+    # ...
+    redis:
+      tls:
+        caSecret: mycasercret
+
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: mycasercret
+  namespace: default
+
+data:
+  # Must contain a certificate under either a `tls.ca` or a `ca.crt` key. 
+  tls.ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.ca=path/to/ca.crt"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    rateLimit:
+      # ... 
+      redis:
+        tls:
+          ca: path/to/ca.crt
+```
+
+```toml tab="File (TOML)"
+[providers.redis.tls]
+  ca = "path/to/ca.crt"
+```
+
+##### `redis.tls.cert`
+
+_Optional_
+
+`cert` is the path to the public certificate used for the secure connection to Redis.
+When this option is set, the `key` option is required.
+
+```yaml tab="Docker & Swarm"
+labels:
+  - "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.cert=path/to/foo.cert"
+  - "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.key=path/to/foo.key"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+   name: test-ratelimit
+spec:
+   rateLimit:
+      # ...
+      redis:
+         tls:
+           certSecret: mytlscert
+
+---
+apiVersion: v1
+kind: Secret
+metadata:
+   name: mytlscert
+   namespace: default
+
+data:
+   tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
+   tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.cert=path/to/foo.cert"
+- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.key=path/to/foo.key"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-ratelimit:
+      rateLimit:
+        redis:
+          tls:
+            cert: path/to/foo.cert
+            key: path/to/foo.key
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-ratelimit.rateLimit]
+    [http.middlewares.test-ratelimit.rateLimit.redis]
+      [http.middlewares.test-ratelimit.rateLimit.redis.tls]
+        cert = "path/to/foo.cert"
+        key = "path/to/foo.key"
+```
+
+##### `redis.tls.key`
+
+_Optional_
+
+`key` is the path to the private key used for the secure connection to Redis.
+When this option is set, the `cert` option is required.
+
+```yaml tab="Docker & Swarm"
+labels:
+  - "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.cert=path/to/foo.cert"
+  - "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.key=path/to/foo.key"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+   name: test-ratelimit
+spec:
+   rateLimit:
+      # ...
+      redis:
+         tls:
+            certSecret: mytlscert
+
+---
+apiVersion: v1
+kind: Secret
+metadata:
+   name: mytlscert
+   namespace: default
+
+data:
+   tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
+   tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.cert=path/to/foo.cert"
+- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.key=path/to/foo.key"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-ratelimit:
+      rateLimit:
+        redis:
+          tls:
+            cert: path/to/foo.cert
+            key: path/to/foo.key
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-ratelimit.rateLimit]
+    [http.middlewares.test-ratelimit.rateLimit.redis]
+      [http.middlewares.test-ratelimit.rateLimit.redis.tls]
+        cert = "path/to/foo.cert"
+        key = "path/to/foo.key"
+```
+
+##### `redis.tls.insecureSkipVerify`
+
+_Optional, Default=false_
+
+If `insecureSkipVerify` is `true`, the TLS connection to Redis accepts any certificate presented by the server regardless of the hostnames it covers.
+
+```yaml tab="Docker & Swarm"
+labels:
+  - "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.insecureSkipVerify=true"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: test-ratelimit
+spec:
+  rateLimit:
+    # ...
+    redis:
+      tls:
+        insecureSkipVerify: true
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.tls.insecureSkipVerify=true"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-ratelimit:
+      rateLimit:
+        # ...
+        redis:
+          tls:
+            insecureSkipVerify: true
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-ratelimit.rateLimit]
+    [http.middlewares.test-ratelimit.rateLimit.redis]
+      [http.middlewares.test-ratelimit.rateLimit.redis.tls]
+        insecureSkipVerify = true
+```
+
+#### `redis.poolSize`
+
+_Optional, Default=0_
+
+Defines the base number of socket connections.
+
+If there are not enough connections in the pool, new connections will be allocated beyond `redis.poolSize`. 
+You can limit this using `redis.maxActiveConns`.
+
+Zero means 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
+
+```yaml tab="Docker & Swarm"
+labels:
+  - "traefik.http.middlewares.test-ratelimit.ratelimit.redis.poolSize=42"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: test-ratelimit
+spec:
+  rateLimit:
+    # ...
+    redis:
+      poolSize: 42
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.poolSize=42"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-ratelimit:
+      rateLimit:
+        # ...
+        redis:
+          poolSize: 42
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-ratelimit.rateLimit]
+    [http.middlewares.test-ratelimit.rateLimit.redis]
+      poolSize = 42
+```
+
+#### `redis.minIdleConns`
+
+_Optional, Default=0_
+
+Defines the minimum number of idle connections, which is useful when establishing new connections is slow.
+Zero means that idle connections are not closed.
+
+```yaml tab="Docker & Swarm"
+labels:
+    - "traefik.http.middlewares.test-ratelimit.ratelimit.redis.minIdleConns=42"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: test-ratelimit
+spec:
+  rateLimit:
+    # ...
+    redis:
+      minIdleConns: 42
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.minIdleConns=42"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-ratelimit:
+      rateLimit:
+        # ...
+        redis:
+          minIdleConns: 42
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-ratelimit.rateLimit]
+    [http.middlewares.test-ratelimit.rateLimit.redis]
+      minIdleConns = 42
+```
+
+#### `redis.maxActiveConns`
+
+_Optional, Default=0_
+
+Defines the maximum number of connections the pool can allocate at a given time.
+Zero means no limit.
+
+```yaml tab="Docker & Swarm"
+labels:
+    - "traefik.http.middlewares.test-ratelimit.ratelimit.redis.maxActiveConns=42"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: test-ratelimit
+spec:
+  rateLimit:
+    # ...
+    redis:
+      maxActiveConns: 42
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.maxActiveConns=42"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-ratelimit:
+      rateLimit:
+        # ...
+        redis:
+          maxActiveConns: 42
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-ratelimit.rateLimit]
+    [http.middlewares.test-ratelimit.rateLimit.redis]
+      maxActiveConns = 42
+```
+
+#### `redis.readTimeout`
+
+_Optional, Default=3s_
+
+Defines the timeout for socket reads. 
+If reached, commands will fail with a timeout instead of blocking.
+Zero means no timeout.
+
+```yaml tab="Docker & Swarm"
+labels:
+  - "traefik.http.middlewares.test-ratelimit.ratelimit.redis.readTimeout=42s"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: test-ratelimit
+spec:
+  rateLimit:
+    # ...
+    redis:
+      readTimeout: 42s
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.readTimeout=42s"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-ratelimit:
+      rateLimit:
+        # ...
+        redis:
+          readTimeout: 42s
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-ratelimit.rateLimit]
+    [http.middlewares.test-ratelimit.rateLimit.redis]
+      readTimeout = "42s"
+```
+
+#### `redis.writeTimeout`
+
+_Optional, Default=3s_
+
+Defines the timeout for socket writes. 
+If reached, commands will fail with a timeout instead of blocking. 
+Zero means no timeout.
+
+```yaml tab="Docker & Swarm"
+labels:
+  - "traefik.http.middlewares.test-ratelimit.ratelimit.redis.writeTimeout=42s"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: test-ratelimit
+spec:
+  rateLimit:
+    # ...
+    redis:
+      writeTimeout: 42s
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.writeTimeout=42s"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-ratelimit:
+      rateLimit:
+        # ...
+        redis:
+          writeTimeout: 42s
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-ratelimit.rateLimit]
+    [http.middlewares.test-ratelimit.rateLimit.redis]
+      writeTimeout = "42s"
+```
+
+#### `redis.dialTimeout`
+
+_Optional, Default=5s_
+
+Defines the dial timeout for establishing new connections.
+Zero means no timeout.
+
+```yaml tab="Docker & Swarm"
+labels:
+  - "traefik.http.middlewares.test-ratelimit.ratelimit.redis.dialTimeout=42s"
+```
+
+```yaml tab="Kubernetes"
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: test-ratelimit
+spec:
+  rateLimit:
+    # ...
+    redis:
+      dialTimeout: 42s
+```
+
+```yaml tab="Consul Catalog"
+- "traefik.http.middlewares.test-ratelimit.ratelimit.redis.dialTimeout=42s"
+```
+
+```yaml tab="File (YAML)"
+http:
+  middlewares:
+    test-ratelimit:
+      rateLimit:
+        # ...
+        redis:
+          dialTimeout: 42s
+```
+
+```toml tab="File (TOML)"
+[http.middlewares]
+  [http.middlewares.test-ratelimit.rateLimit]
+    [http.middlewares.test-ratelimit.rateLimit.redis]
+      dialTimeout = "42s"
+```
diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml
index baf1c4186a..bc8b268f91 100644
--- a/docs/content/reference/dynamic-configuration/docker-labels.yml
+++ b/docs/content/reference/dynamic-configuration/docker-labels.yml
@@ -132,6 +132,20 @@
 - "traefik.http.middlewares.middleware18.ratelimit.average=42"
 - "traefik.http.middlewares.middleware18.ratelimit.burst=42"
 - "traefik.http.middlewares.middleware18.ratelimit.period=42s"
+- "traefik.http.middlewares.middleware18.ratelimit.redis.db=42"
+- "traefik.http.middlewares.middleware18.ratelimit.redis.dialtimeout=42s"
+- "traefik.http.middlewares.middleware18.ratelimit.redis.endpoints=foobar, foobar"
+- "traefik.http.middlewares.middleware18.ratelimit.redis.maxactiveconns=42"
+- "traefik.http.middlewares.middleware18.ratelimit.redis.minidleconns=42"
+- "traefik.http.middlewares.middleware18.ratelimit.redis.password=foobar"
+- "traefik.http.middlewares.middleware18.ratelimit.redis.poolsize=42"
+- "traefik.http.middlewares.middleware18.ratelimit.redis.readtimeout=42s"
+- "traefik.http.middlewares.middleware18.ratelimit.redis.tls.ca=foobar"
+- "traefik.http.middlewares.middleware18.ratelimit.redis.tls.cert=foobar"
+- "traefik.http.middlewares.middleware18.ratelimit.redis.tls.insecureskipverify=true"
+- "traefik.http.middlewares.middleware18.ratelimit.redis.tls.key=foobar"
+- "traefik.http.middlewares.middleware18.ratelimit.redis.username=foobar"
+- "traefik.http.middlewares.middleware18.ratelimit.redis.writetimeout=42s"
 - "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.ipstrategy.depth=42"
 - "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.ipstrategy.excludedips=foobar, foobar"
 - "traefik.http.middlewares.middleware18.ratelimit.sourcecriterion.ipstrategy.ipv6subnet=42"
diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml
index a5d45447b0..0c5ae3bbdf 100644
--- a/docs/content/reference/dynamic-configuration/file.toml
+++ b/docs/content/reference/dynamic-configuration/file.toml
@@ -311,6 +311,22 @@
             depth = 42
             excludedIPs = ["foobar", "foobar"]
             ipv6Subnet = 42
+        [http.middlewares.Middleware18.rateLimit.redis]
+          endpoints = ["foobar", "foobar"]
+          username = "foobar"
+          password = "foobar"
+          db = 42
+          poolSize = 42
+          minIdleConns = 42
+          maxActiveConns = 42
+          readTimeout = "42s"
+          writeTimeout = "42s"
+          dialTimeout = "42s"
+          [http.middlewares.Middleware18.rateLimit.redis.tls]
+            ca = "foobar"
+            cert = "foobar"
+            key = "foobar"
+            insecureSkipVerify = true
     [http.middlewares.Middleware19]
       [http.middlewares.Middleware19.redirectRegex]
         regex = "foobar"
diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml
index c7daacc11e..7c0766810b 100644
--- a/docs/content/reference/dynamic-configuration/file.yaml
+++ b/docs/content/reference/dynamic-configuration/file.yaml
@@ -360,6 +360,24 @@ http:
             ipv6Subnet: 42
           requestHeaderName: foobar
           requestHost: true
+        redis:
+          endpoints:
+            - foobar
+            - foobar
+          tls:
+            ca: foobar
+            cert: foobar
+            key: foobar
+            insecureSkipVerify: true
+          username: foobar
+          password: foobar
+          db: 42
+          poolSize: 42
+          minIdleConns: 42
+          maxActiveConns: 42
+          readTimeout: 42s
+          writeTimeout: 42s
+          dialTimeout: 42s
     Middleware19:
       redirectRegex:
         regex: foobar
diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml
index 43b14722c1..0b5982e204 100644
--- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml
+++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml
@@ -1790,6 +1790,90 @@ spec:
                       Period, in combination with Average, defines the actual maximum rate, such as:
                       r = Average / Period. It defaults to a second.
                     x-kubernetes-int-or-string: true
+                  redis:
+                    description: Redis hold the configs of Redis as bucket in rate
+                      limiter.
+                    properties:
+                      db:
+                        description: DB defines the Redis database that will be selected
+                          after connecting to the server.
+                        type: integer
+                      dialTimeout:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        description: |-
+                          DialTimeout sets the timeout for establishing new connections.
+                          Default value is 5 seconds.
+                        pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
+                        x-kubernetes-int-or-string: true
+                      endpoints:
+                        description: |-
+                          Endpoints contains either a single address or a seed list of host:port addresses.
+                          Default value is ["localhost:6379"].
+                        items:
+                          type: string
+                        type: array
+                      maxActiveConns:
+                        description: |-
+                          MaxActiveConns defines the maximum number of connections allocated by the pool at a given time.
+                          Default value is 0, meaning there is no limit.
+                        type: integer
+                      minIdleConns:
+                        description: |-
+                          MinIdleConns defines the minimum number of idle connections.
+                          Default value is 0, and idle connections are not closed by default.
+                        type: integer
+                      poolSize:
+                        description: |-
+                          PoolSize defines the initial number of socket connections.
+                          If the pool runs out of available connections, additional ones will be created beyond PoolSize.
+                          This can be limited using MaxActiveConns.
+                          // Default value is 0, meaning 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
+                        type: integer
+                      readTimeout:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        description: |-
+                          ReadTimeout defines the timeout for socket read operations.
+                          Default value is 3 seconds.
+                        pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
+                        x-kubernetes-int-or-string: true
+                      secret:
+                        description: Secret defines the name of the referenced Kubernetes
+                          Secret containing Redis credentials.
+                        type: string
+                      tls:
+                        description: |-
+                          TLS defines TLS-specific configurations, including the CA, certificate, and key,
+                          which can be provided as a file path or file content.
+                        properties:
+                          caSecret:
+                            description: |-
+                              CASecret is the name of the referenced Kubernetes Secret containing the CA to validate the server certificate.
+                              The CA certificate is extracted from key `tls.ca` or `ca.crt`.
+                            type: string
+                          certSecret:
+                            description: |-
+                              CertSecret is the name of the referenced Kubernetes Secret containing the client certificate.
+                              The client certificate is extracted from the keys `tls.crt` and `tls.key`.
+                            type: string
+                          insecureSkipVerify:
+                            description: InsecureSkipVerify defines whether the server
+                              certificates should be validated.
+                            type: boolean
+                        type: object
+                      writeTimeout:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        description: |-
+                          WriteTimeout defines the timeout for socket write operations.
+                          Default value is 3 seconds.
+                        pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
+                        x-kubernetes-int-or-string: true
+                    type: object
                   sourceCriterion:
                     description: |-
                       SourceCriterion defines what criterion is used to group requests as originating from a common source.
diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md
index e04f5b84ea..c1a0809255 100644
--- a/docs/content/reference/dynamic-configuration/kv-ref.md
+++ b/docs/content/reference/dynamic-configuration/kv-ref.md
@@ -153,6 +153,21 @@ THIS FILE MUST NOT BE EDITED BY HAND
 | `traefik/http/middlewares/Middleware18/rateLimit/average` | `42` |
 | `traefik/http/middlewares/Middleware18/rateLimit/burst` | `42` |
 | `traefik/http/middlewares/Middleware18/rateLimit/period` | `42s` |
+| `traefik/http/middlewares/Middleware18/rateLimit/redis/db` | `42` |
+| `traefik/http/middlewares/Middleware18/rateLimit/redis/dialTimeout` | `42s` |
+| `traefik/http/middlewares/Middleware18/rateLimit/redis/endpoints/0` | `foobar` |
+| `traefik/http/middlewares/Middleware18/rateLimit/redis/endpoints/1` | `foobar` |
+| `traefik/http/middlewares/Middleware18/rateLimit/redis/maxActiveConns` | `42` |
+| `traefik/http/middlewares/Middleware18/rateLimit/redis/minIdleConns` | `42` |
+| `traefik/http/middlewares/Middleware18/rateLimit/redis/password` | `foobar` |
+| `traefik/http/middlewares/Middleware18/rateLimit/redis/poolSize` | `42` |
+| `traefik/http/middlewares/Middleware18/rateLimit/redis/readTimeout` | `42s` |
+| `traefik/http/middlewares/Middleware18/rateLimit/redis/tls/ca` | `foobar` |
+| `traefik/http/middlewares/Middleware18/rateLimit/redis/tls/cert` | `foobar` |
+| `traefik/http/middlewares/Middleware18/rateLimit/redis/tls/insecureSkipVerify` | `true` |
+| `traefik/http/middlewares/Middleware18/rateLimit/redis/tls/key` | `foobar` |
+| `traefik/http/middlewares/Middleware18/rateLimit/redis/username` | `foobar` |
+| `traefik/http/middlewares/Middleware18/rateLimit/redis/writeTimeout` | `42s` |
 | `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/ipStrategy/depth` | `42` |
 | `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/ipStrategy/excludedIPs/0` | `foobar` |
 | `traefik/http/middlewares/Middleware18/rateLimit/sourceCriterion/ipStrategy/excludedIPs/1` | `foobar` |
diff --git a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml
index c5fe517a97..cf0da07197 100644
--- a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml
+++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml
@@ -1027,6 +1027,90 @@ spec:
                       Period, in combination with Average, defines the actual maximum rate, such as:
                       r = Average / Period. It defaults to a second.
                     x-kubernetes-int-or-string: true
+                  redis:
+                    description: Redis hold the configs of Redis as bucket in rate
+                      limiter.
+                    properties:
+                      db:
+                        description: DB defines the Redis database that will be selected
+                          after connecting to the server.
+                        type: integer
+                      dialTimeout:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        description: |-
+                          DialTimeout sets the timeout for establishing new connections.
+                          Default value is 5 seconds.
+                        pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
+                        x-kubernetes-int-or-string: true
+                      endpoints:
+                        description: |-
+                          Endpoints contains either a single address or a seed list of host:port addresses.
+                          Default value is ["localhost:6379"].
+                        items:
+                          type: string
+                        type: array
+                      maxActiveConns:
+                        description: |-
+                          MaxActiveConns defines the maximum number of connections allocated by the pool at a given time.
+                          Default value is 0, meaning there is no limit.
+                        type: integer
+                      minIdleConns:
+                        description: |-
+                          MinIdleConns defines the minimum number of idle connections.
+                          Default value is 0, and idle connections are not closed by default.
+                        type: integer
+                      poolSize:
+                        description: |-
+                          PoolSize defines the initial number of socket connections.
+                          If the pool runs out of available connections, additional ones will be created beyond PoolSize.
+                          This can be limited using MaxActiveConns.
+                          // Default value is 0, meaning 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
+                        type: integer
+                      readTimeout:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        description: |-
+                          ReadTimeout defines the timeout for socket read operations.
+                          Default value is 3 seconds.
+                        pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
+                        x-kubernetes-int-or-string: true
+                      secret:
+                        description: Secret defines the name of the referenced Kubernetes
+                          Secret containing Redis credentials.
+                        type: string
+                      tls:
+                        description: |-
+                          TLS defines TLS-specific configurations, including the CA, certificate, and key,
+                          which can be provided as a file path or file content.
+                        properties:
+                          caSecret:
+                            description: |-
+                              CASecret is the name of the referenced Kubernetes Secret containing the CA to validate the server certificate.
+                              The CA certificate is extracted from key `tls.ca` or `ca.crt`.
+                            type: string
+                          certSecret:
+                            description: |-
+                              CertSecret is the name of the referenced Kubernetes Secret containing the client certificate.
+                              The client certificate is extracted from the keys `tls.crt` and `tls.key`.
+                            type: string
+                          insecureSkipVerify:
+                            description: InsecureSkipVerify defines whether the server
+                              certificates should be validated.
+                            type: boolean
+                        type: object
+                      writeTimeout:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        description: |-
+                          WriteTimeout defines the timeout for socket write operations.
+                          Default value is 3 seconds.
+                        pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
+                        x-kubernetes-int-or-string: true
+                    type: object
                   sourceCriterion:
                     description: |-
                       SourceCriterion defines what criterion is used to group requests as originating from a common source.
diff --git a/go.mod b/go.mod
index 6e9528a40e..7493f9f505 100644
--- a/go.mod
+++ b/go.mod
@@ -50,6 +50,7 @@ require (
 	github.com/prometheus/client_golang v1.19.1
 	github.com/prometheus/client_model v0.6.1
 	github.com/quic-go/quic-go v0.48.2
+	github.com/redis/go-redis/v9 v9.7.1
 	github.com/rs/zerolog v1.33.0
 	github.com/sirupsen/logrus v1.9.3
 	github.com/spiffe/go-spiffe/v2 v2.4.0
@@ -70,6 +71,7 @@ require (
 	github.com/valyala/fasthttp v1.58.0
 	github.com/vulcand/oxy/v2 v2.0.0
 	github.com/vulcand/predicate v1.2.0
+	github.com/yuin/gopher-lua v1.1.1
 	go.opentelemetry.io/collector/pdata v1.10.0
 	go.opentelemetry.io/contrib/bridges/otellogrus v0.7.0
 	go.opentelemetry.io/contrib/propagators/autoprop v0.53.0
@@ -301,7 +303,6 @@ require (
 	github.com/prometheus/common v0.55.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/quic-go/qpack v0.5.1 // indirect
-	github.com/redis/go-redis/v9 v9.6.1 // indirect
 	github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
 	github.com/rs/cors v1.7.0 // indirect
 	github.com/sacloud/api-client-go v0.2.10 // indirect
diff --git a/go.sum b/go.sum
index 6c0789d4cf..5ed12b4284 100644
--- a/go.sum
+++ b/go.sum
@@ -1030,8 +1030,8 @@ github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KW
 github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
-github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
+github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc=
+github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
 github.com/regfish/regfish-dnsapi-go v0.1.1 h1:TJFtbePHkd47q5GZwYl1h3DIYXmoxdLjW/SBsPtB5IE=
 github.com/regfish/regfish-dnsapi-go v0.1.1/go.mod h1:ubIgXSfqarSnl3XHSn8hIFwFF3h0yrq0ZiWD93Y2VjY=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
@@ -1256,6 +1256,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
+github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
 github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
 github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
 github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml
index 43b14722c1..0b5982e204 100644
--- a/integration/fixtures/k8s/01-traefik-crd.yml
+++ b/integration/fixtures/k8s/01-traefik-crd.yml
@@ -1790,6 +1790,90 @@ spec:
                       Period, in combination with Average, defines the actual maximum rate, such as:
                       r = Average / Period. It defaults to a second.
                     x-kubernetes-int-or-string: true
+                  redis:
+                    description: Redis hold the configs of Redis as bucket in rate
+                      limiter.
+                    properties:
+                      db:
+                        description: DB defines the Redis database that will be selected
+                          after connecting to the server.
+                        type: integer
+                      dialTimeout:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        description: |-
+                          DialTimeout sets the timeout for establishing new connections.
+                          Default value is 5 seconds.
+                        pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
+                        x-kubernetes-int-or-string: true
+                      endpoints:
+                        description: |-
+                          Endpoints contains either a single address or a seed list of host:port addresses.
+                          Default value is ["localhost:6379"].
+                        items:
+                          type: string
+                        type: array
+                      maxActiveConns:
+                        description: |-
+                          MaxActiveConns defines the maximum number of connections allocated by the pool at a given time.
+                          Default value is 0, meaning there is no limit.
+                        type: integer
+                      minIdleConns:
+                        description: |-
+                          MinIdleConns defines the minimum number of idle connections.
+                          Default value is 0, and idle connections are not closed by default.
+                        type: integer
+                      poolSize:
+                        description: |-
+                          PoolSize defines the initial number of socket connections.
+                          If the pool runs out of available connections, additional ones will be created beyond PoolSize.
+                          This can be limited using MaxActiveConns.
+                          // Default value is 0, meaning 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
+                        type: integer
+                      readTimeout:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        description: |-
+                          ReadTimeout defines the timeout for socket read operations.
+                          Default value is 3 seconds.
+                        pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
+                        x-kubernetes-int-or-string: true
+                      secret:
+                        description: Secret defines the name of the referenced Kubernetes
+                          Secret containing Redis credentials.
+                        type: string
+                      tls:
+                        description: |-
+                          TLS defines TLS-specific configurations, including the CA, certificate, and key,
+                          which can be provided as a file path or file content.
+                        properties:
+                          caSecret:
+                            description: |-
+                              CASecret is the name of the referenced Kubernetes Secret containing the CA to validate the server certificate.
+                              The CA certificate is extracted from key `tls.ca` or `ca.crt`.
+                            type: string
+                          certSecret:
+                            description: |-
+                              CertSecret is the name of the referenced Kubernetes Secret containing the client certificate.
+                              The client certificate is extracted from the keys `tls.crt` and `tls.key`.
+                            type: string
+                          insecureSkipVerify:
+                            description: InsecureSkipVerify defines whether the server
+                              certificates should be validated.
+                            type: boolean
+                        type: object
+                      writeTimeout:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        description: |-
+                          WriteTimeout defines the timeout for socket write operations.
+                          Default value is 3 seconds.
+                        pattern: ^([0-9]+(ns|us|µs|ms|s|m|h)?)+$
+                        x-kubernetes-int-or-string: true
+                    type: object
                   sourceCriterion:
                     description: |-
                       SourceCriterion defines what criterion is used to group requests as originating from a common source.
diff --git a/integration/fixtures/ratelimit/simple_redis.toml b/integration/fixtures/ratelimit/simple_redis.toml
new file mode 100644
index 0000000000..cf3f08b651
--- /dev/null
+++ b/integration/fixtures/ratelimit/simple_redis.toml
@@ -0,0 +1,39 @@
+[global]
+  checkNewVersion = false
+  sendAnonymousUsage = false
+
+[api]
+  insecure = true
+
+[log]
+  level = "DEBUG"
+  noColor = true
+
+[entryPoints]
+  [entryPoints.web]
+    address = ":8081"
+
+[providers.file]
+  filename = "{{ .SelfFilename }}"
+
+## dynamic configuration ##
+
+[http.routers]
+  [http.routers.router1]
+    service = "service1"
+    middlewares = [ "ratelimit" ]
+    rule = "Path(`/`)"
+
+[http.middlewares]
+  [http.middlewares.ratelimit.rateLimit]
+    average = 100
+    burst = 1
+    [http.middlewares.ratelimit.rateLimit.redis]
+      endpoints = ["{{ .RedisEndpoint }}"]
+
+[http.services]
+  [http.services.service1]
+    [http.services.service1.loadBalancer]
+      passHostHeader = true
+      [[http.services.service1.loadBalancer.servers]]
+        url = "http://{{.Server1}}:80"
diff --git a/integration/resources/compose/ratelimit.yml b/integration/resources/compose/ratelimit.yml
index 2515681655..5d9d6ec1e2 100644
--- a/integration/resources/compose/ratelimit.yml
+++ b/integration/resources/compose/ratelimit.yml
@@ -2,3 +2,10 @@ version: "3.8"
 services:
   whoami1:
     image: traefik/whoami
+
+  redis:
+    image: redis:5.0
+    command:
+      - redis-server
+      - --port
+      - 6379
diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go
index e1fc3a73b2..501b75c01b 100644
--- a/pkg/config/dynamic/middlewares.go
+++ b/pkg/config/dynamic/middlewares.go
@@ -7,6 +7,7 @@ import (
 
 	ptypes "github.com/traefik/paerser/types"
 	"github.com/traefik/traefik/v3/pkg/ip"
+	"github.com/traefik/traefik/v3/pkg/types"
 )
 
 // ForwardAuthDefaultMaxBodySize is the ForwardAuth.MaxBodySize option default value.
@@ -566,6 +567,10 @@ type RateLimit struct {
 	// If several strategies are defined at the same time, an error will be raised.
 	// If none are set, the default is to use the request's remote address field (as an ipStrategy).
 	SourceCriterion *SourceCriterion `json:"sourceCriterion,omitempty" toml:"sourceCriterion,omitempty" yaml:"sourceCriterion,omitempty" export:"true"`
+
+	// Redis stores the configuration for using Redis as a bucket in the rate-limiting algorithm.
+	// If not specified, Traefik will default to an in-memory bucket for the algorithm.
+	Redis *Redis `json:"redis,omitempty" toml:"redis,omitempty" yaml:"redis,omitempty" export:"true"`
 }
 
 // SetDefaults sets the default values on a RateLimit.
@@ -576,6 +581,58 @@ func (r *RateLimit) SetDefaults() {
 
 // +k8s:deepcopy-gen=true
 
+// Redis holds the Redis configuration.
+type Redis struct {
+	// Endpoints contains either a single address or a seed list of host:port addresses.
+	// Default value is ["localhost:6379"].
+	Endpoints []string `json:"endpoints,omitempty" toml:"endpoints,omitempty" yaml:"endpoints,omitempty"`
+	// TLS defines TLS-specific configurations, including the CA, certificate, and key,
+	// which can be provided as a file path or file content.
+	TLS *types.ClientTLS `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`
+	// Username defines the username to connect to the Redis server.
+	Username string `json:"username,omitempty" toml:"username,omitempty" yaml:"username,omitempty" loggable:"false"`
+	// Password defines the password to connect to the Redis server.
+	Password string `json:"password,omitempty" toml:"password,omitempty" yaml:"password,omitempty" loggable:"false"`
+	// DB defines the Redis database that will be selected after connecting to the server.
+	DB int `json:"db,omitempty" toml:"db,omitempty" yaml:"db,omitempty"`
+	// PoolSize defines the initial number of socket connections.
+	// If the pool runs out of available connections, additional ones will be created beyond PoolSize.
+	// This can be limited using MaxActiveConns.
+	// Default value is 0, meaning 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
+	PoolSize int `json:"poolSize,omitempty" toml:"poolSize,omitempty" yaml:"poolSize,omitempty" export:"true"`
+	// MinIdleConns defines the minimum number of idle connections.
+	// Default value is 0, and idle connections are not closed by default.
+	MinIdleConns int `json:"minIdleConns,omitempty" toml:"minIdleConns,omitempty" yaml:"minIdleConns,omitempty" export:"true"`
+	// MaxActiveConns defines the maximum number of connections allocated by the pool at a given time.
+	// Default value is 0, meaning there is no limit.
+	MaxActiveConns int `json:"maxActiveConns,omitempty" toml:"maxActiveConns,omitempty" yaml:"maxActiveConns,omitempty" export:"true"`
+	// ReadTimeout defines the timeout for socket read operations.
+	// Default value is 3 seconds.
+	ReadTimeout *ptypes.Duration `json:"readTimeout,omitempty" toml:"readTimeout,omitempty" yaml:"readTimeout,omitempty" export:"true"`
+	// WriteTimeout defines the timeout for socket write operations.
+	// Default value is 3 seconds.
+	WriteTimeout *ptypes.Duration `json:"writeTimeout,omitempty" toml:"writeTimeout,omitempty" yaml:"writeTimeout,omitempty" export:"true"`
+	// DialTimeout sets the timeout for establishing new connections.
+	// Default value is 5 seconds.
+	DialTimeout *ptypes.Duration `json:"dialTimeout,omitempty" toml:"dialTimeout,omitempty" yaml:"dialTimeout,omitempty" export:"true"`
+}
+
+// SetDefaults sets the default values on a RateLimit.
+func (r *Redis) SetDefaults() {
+	r.Endpoints = []string{"localhost:6379"}
+
+	defaultReadTimeout := ptypes.Duration(3 * time.Second)
+	r.ReadTimeout = &defaultReadTimeout
+
+	defaultWriteTimeout := ptypes.Duration(3 * time.Second)
+	r.WriteTimeout = &defaultWriteTimeout
+
+	defaultDialTimeout := ptypes.Duration(5 * time.Second)
+	r.DialTimeout = &defaultDialTimeout
+}
+
+// +k8s:deepcopy-gen=true
+
 // RedirectRegex holds the redirect regex middleware configuration.
 // This middleware redirects a request using regex matching and replacement.
 // More info: https://doc.traefik.io/traefik/v3.3/middlewares/http/redirectregex/#regex
diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go
index 2f0e8dd4d0..2a680897c9 100644
--- a/pkg/config/dynamic/zz_generated.deepcopy.go
+++ b/pkg/config/dynamic/zz_generated.deepcopy.go
@@ -30,6 +30,7 @@ THE SOFTWARE.
 package dynamic
 
 import (
+	paersertypes "github.com/traefik/paerser/types"
 	tls "github.com/traefik/traefik/v3/pkg/tls"
 	types "github.com/traefik/traefik/v3/pkg/types"
 )
@@ -1094,6 +1095,11 @@ func (in *RateLimit) DeepCopyInto(out *RateLimit) {
 		*out = new(SourceCriterion)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Redis != nil {
+		in, out := &in.Redis, &out.Redis
+		*out = new(Redis)
+		(*in).DeepCopyInto(*out)
+	}
 	return
 }
 
@@ -1139,6 +1145,47 @@ func (in *RedirectScheme) DeepCopy() *RedirectScheme {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Redis) DeepCopyInto(out *Redis) {
+	*out = *in
+	if in.Endpoints != nil {
+		in, out := &in.Endpoints, &out.Endpoints
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.TLS != nil {
+		in, out := &in.TLS, &out.TLS
+		*out = new(types.ClientTLS)
+		**out = **in
+	}
+	if in.ReadTimeout != nil {
+		in, out := &in.ReadTimeout, &out.ReadTimeout
+		*out = new(paersertypes.Duration)
+		**out = **in
+	}
+	if in.WriteTimeout != nil {
+		in, out := &in.WriteTimeout, &out.WriteTimeout
+		*out = new(paersertypes.Duration)
+		**out = **in
+	}
+	if in.DialTimeout != nil {
+		in, out := &in.DialTimeout, &out.DialTimeout
+		*out = new(paersertypes.Duration)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Redis.
+func (in *Redis) DeepCopy() *Redis {
+	if in == nil {
+		return nil
+	}
+	out := new(Redis)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ReplacePath) DeepCopyInto(out *ReplacePath) {
 	*out = *in
diff --git a/pkg/middlewares/ratelimiter/in_memory_limiter.go b/pkg/middlewares/ratelimiter/in_memory_limiter.go
new file mode 100644
index 0000000000..bec67aa077
--- /dev/null
+++ b/pkg/middlewares/ratelimiter/in_memory_limiter.go
@@ -0,0 +1,72 @@
+package ratelimiter
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/mailgun/ttlmap"
+	"github.com/rs/zerolog"
+	"golang.org/x/time/rate"
+)
+
+type inMemoryRateLimiter struct {
+	rate  rate.Limit // reqs/s
+	burst int64
+	// maxDelay is the maximum duration we're willing to wait for a bucket reservation to become effective, in nanoseconds.
+	// For now it is somewhat arbitrarily set to 1/(2*rate).
+	maxDelay time.Duration
+	// Each rate limiter for a given source is stored in the buckets ttlmap.
+	// To keep this ttlmap constrained in size,
+	// each ratelimiter is "garbage collected" when it is considered expired.
+	// It is considered expired after it hasn't been used for ttl seconds.
+	ttl     int
+	buckets *ttlmap.TtlMap // actual buckets, keyed by source.
+
+	logger *zerolog.Logger
+}
+
+func newInMemoryRateLimiter(rate rate.Limit, burst int64, maxDelay time.Duration, ttl int, logger *zerolog.Logger) (*inMemoryRateLimiter, error) {
+	buckets, err := ttlmap.NewConcurrent(maxSources)
+	if err != nil {
+		return nil, fmt.Errorf("creating ttlmap: %w", err)
+	}
+
+	return &inMemoryRateLimiter{
+		rate:     rate,
+		burst:    burst,
+		maxDelay: maxDelay,
+		ttl:      ttl,
+		logger:   logger,
+		buckets:  buckets,
+	}, nil
+}
+
+func (i *inMemoryRateLimiter) Allow(_ context.Context, source string) (*time.Duration, error) {
+	// Get bucket which contains limiter information.
+	var bucket *rate.Limiter
+	if rlSource, exists := i.buckets.Get(source); exists {
+		bucket = rlSource.(*rate.Limiter)
+	} else {
+		bucket = rate.NewLimiter(i.rate, int(i.burst))
+	}
+
+	// We Set even in the case where the source already exists,
+	// because we want to update the expiryTime everytime we get the source,
+	// as the expiryTime is supposed to reflect the activity (or lack thereof) on that source.
+	if err := i.buckets.Set(source, bucket, i.ttl); err != nil {
+		return nil, fmt.Errorf("setting buckets: %w", err)
+	}
+
+	res := bucket.Reserve()
+	if !res.OK() {
+		return nil, nil
+	}
+
+	delay := res.Delay()
+	if delay > i.maxDelay {
+		res.Cancel()
+	}
+
+	return &delay, nil
+}
diff --git a/pkg/middlewares/ratelimiter/lua.go b/pkg/middlewares/ratelimiter/lua.go
new file mode 100644
index 0000000000..47100629ec
--- /dev/null
+++ b/pkg/middlewares/ratelimiter/lua.go
@@ -0,0 +1,66 @@
+package ratelimiter
+
+import (
+	"context"
+
+	"github.com/redis/go-redis/v9"
+)
+
+type Rediser interface {
+	Eval(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd
+	EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd
+	ScriptExists(ctx context.Context, hashes ...string) *redis.BoolSliceCmd
+	ScriptLoad(ctx context.Context, script string) *redis.StringCmd
+	Del(ctx context.Context, keys ...string) *redis.IntCmd
+
+	EvalRO(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd
+	EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd
+}
+
+//nolint:dupword
+var AllowTokenBucketRaw = `
+local key = KEYS[1]
+local limit, burst, ttl, t, max_delay = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3]), tonumber(ARGV[4]),
+    tonumber(ARGV[5])
+
+local bucket = {
+    limit = limit,
+    burst = burst,
+    tokens = 0,
+    last = 0
+}
+
+local rl_source = redis.call('hgetall', key)
+
+if table.maxn(rl_source) == 4 then
+    -- Get bucket state from redis
+    bucket.last = tonumber(rl_source[2])
+    bucket.tokens = tonumber(rl_source[4])
+end
+
+local last = bucket.last
+if t < last then
+    last = t
+end
+
+local elapsed = t - last
+local delta = bucket.limit * elapsed
+local tokens = bucket.tokens + delta
+tokens = math.min(tokens, bucket.burst)
+tokens = tokens - 1
+
+local wait_duration = 0
+if tokens < 0 then
+    wait_duration = (tokens * -1) / bucket.limit
+    if wait_duration > max_delay then
+        tokens = tokens + 1
+        tokens = math.min(tokens, burst)
+    end
+end
+
+redis.call('hset', key, 'last', t, 'tokens', tokens)
+redis.call('expire', key, ttl)
+
+return {tostring(true), tostring(wait_duration),tostring(tokens)}`
+
+var AllowTokenBucketScript = redis.NewScript(AllowTokenBucketRaw)
diff --git a/pkg/middlewares/ratelimiter/rate_limiter.go b/pkg/middlewares/ratelimiter/rate_limiter.go
old mode 100644
new mode 100755
index d22b99a422..5798fd87a4
--- a/pkg/middlewares/ratelimiter/rate_limiter.go
+++ b/pkg/middlewares/ratelimiter/rate_limiter.go
@@ -8,7 +8,7 @@ import (
 	"net/http"
 	"time"
 
-	"github.com/mailgun/ttlmap"
+	"github.com/rs/zerolog"
 	"github.com/rs/zerolog/log"
 	"github.com/traefik/traefik/v3/pkg/config/dynamic"
 	"github.com/traefik/traefik/v3/pkg/middlewares"
@@ -23,24 +23,23 @@ const (
 	maxSources = 65536
 )
 
+type limiter interface {
+	Allow(ctx context.Context, token string) (*time.Duration, error)
+}
+
 // rateLimiter implements rate limiting and traffic shaping with a set of token buckets;
 // one for each traffic source. The same parameters are applied to all the buckets.
 type rateLimiter struct {
-	name  string
-	rate  rate.Limit // reqs/s
-	burst int64
+	name string
+	rate rate.Limit // reqs/s
 	// maxDelay is the maximum duration we're willing to wait for a bucket reservation to become effective, in nanoseconds.
 	// For now it is somewhat arbitrarily set to 1/(2*rate).
-	maxDelay time.Duration
-	// each rate limiter for a given source is stored in the buckets ttlmap.
-	// To keep this ttlmap constrained in size,
-	// each ratelimiter is "garbage collected" when it is considered expired.
-	// It is considered expired after it hasn't been used for ttl seconds.
-	ttl           int
+	maxDelay      time.Duration
 	sourceMatcher utils.SourceExtractor
 	next          http.Handler
+	logger        *zerolog.Logger
 
-	buckets *ttlmap.TtlMap // actual buckets, keyed by source.
+	limiter limiter
 }
 
 // New returns a rate limiter middleware.
@@ -60,12 +59,7 @@ func New(ctx context.Context, next http.Handler, config dynamic.RateLimit, name
 
 	sourceMatcher, err := middlewares.GetSourceExtractor(ctxLog, config.SourceCriterion)
 	if err != nil {
-		return nil, err
-	}
-
-	buckets, err := ttlmap.NewConcurrent(maxSources)
-	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("getting source extractor: %w", err)
 	}
 
 	burst := config.Burst
@@ -109,16 +103,27 @@ func New(ctx context.Context, next http.Handler, config dynamic.RateLimit, name
 	} else if rtl > 0 {
 		ttl += int(1 / rtl)
 	}
+	var limiter limiter
+	if config.Redis != nil {
+		limiter, err = newRedisLimiter(ctx, rate.Limit(rtl), burst, maxDelay, ttl, config, logger)
+		if err != nil {
+			return nil, fmt.Errorf("creating redis limiter: %w", err)
+		}
+	} else {
+		limiter, err = newInMemoryRateLimiter(rate.Limit(rtl), burst, maxDelay, ttl, logger)
+		if err != nil {
+			return nil, fmt.Errorf("creating in-memory limiter: %w", err)
+		}
+	}
 
 	return &rateLimiter{
+		logger:        logger,
 		name:          name,
 		rate:          rate.Limit(rtl),
-		burst:         burst,
 		maxDelay:      maxDelay,
 		next:          next,
 		sourceMatcher: sourceMatcher,
-		buckets:       buckets,
-		ttl:           ttl,
+		limiter:       limiter,
 	}, nil
 }
 
@@ -141,38 +146,34 @@ func (rl *rateLimiter) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 		logger.Info().Msgf("ignoring token bucket amount > 1: %d", amount)
 	}
 
-	var bucket *rate.Limiter
-	if rlSource, exists := rl.buckets.Get(source); exists {
-		bucket = rlSource.(*rate.Limiter)
-	} else {
-		bucket = rate.NewLimiter(rl.rate, int(rl.burst))
+	delay, err := rl.limiter.Allow(ctx, source)
+	if err != nil {
+		rl.logger.Error().Err(err).Msg("Could not insert/update bucket")
+		observability.SetStatusErrorf(ctx, "Could not insert/update bucket")
+		http.Error(rw, "Could not insert/update bucket", http.StatusInternalServerError)
+		return
 	}
 
-	// We Set even in the case where the source already exists,
-	// because we want to update the expiryTime every time we get the source,
-	// as the expiryTime is supposed to reflect the activity (or lack thereof) on that source.
-	if err := rl.buckets.Set(source, bucket, rl.ttl); err != nil {
-		logger.Error().Err(err).Msg("Could not insert/update bucket")
-		observability.SetStatusErrorf(req.Context(), "Could not insert/update bucket")
-		http.Error(rw, "could not insert/update bucket", http.StatusInternalServerError)
+	if delay == nil {
+		observability.SetStatusErrorf(ctx, "No bursty traffic allowed")
+		http.Error(rw, "No bursty traffic allowed", http.StatusTooManyRequests)
 		return
 	}
 
-	res := bucket.Reserve()
-	if !res.OK() {
-		observability.SetStatusErrorf(req.Context(), "No bursty traffic allowed")
-		http.Error(rw, "No bursty traffic allowed", http.StatusTooManyRequests)
+	if *delay > rl.maxDelay {
+		rl.serveDelayError(ctx, rw, *delay)
 		return
 	}
 
-	delay := res.Delay()
-	if delay > rl.maxDelay {
-		res.Cancel()
-		rl.serveDelayError(ctx, rw, delay)
+	select {
+	case <-ctx.Done():
+		observability.SetStatusErrorf(ctx, "Context canceled")
+		http.Error(rw, "context canceled", http.StatusInternalServerError)
 		return
+
+	case <-time.After(*delay):
 	}
 
-	time.Sleep(delay)
 	rl.next.ServeHTTP(rw, req)
 }
 
diff --git a/pkg/middlewares/ratelimiter/redis_limiter.go b/pkg/middlewares/ratelimiter/redis_limiter.go
new file mode 100644
index 0000000000..6bdf6c8fca
--- /dev/null
+++ b/pkg/middlewares/ratelimiter/redis_limiter.go
@@ -0,0 +1,118 @@
+package ratelimiter
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"time"
+
+	"github.com/redis/go-redis/v9"
+	"github.com/rs/zerolog"
+	ptypes "github.com/traefik/paerser/types"
+	"github.com/traefik/traefik/v3/pkg/config/dynamic"
+	"golang.org/x/time/rate"
+)
+
+const redisPrefix = "rate:"
+
+type redisLimiter struct {
+	rate     rate.Limit // reqs/s
+	burst    int64
+	maxDelay time.Duration
+	period   ptypes.Duration
+	logger   *zerolog.Logger
+	ttl      int
+	client   Rediser
+}
+
+func newRedisLimiter(ctx context.Context, rate rate.Limit, burst int64, maxDelay time.Duration, ttl int, config dynamic.RateLimit, logger *zerolog.Logger) (limiter, error) {
+	options := &redis.UniversalOptions{
+		Addrs:          config.Redis.Endpoints,
+		Username:       config.Redis.Username,
+		Password:       config.Redis.Password,
+		DB:             config.Redis.DB,
+		PoolSize:       config.Redis.PoolSize,
+		MinIdleConns:   config.Redis.MinIdleConns,
+		MaxActiveConns: config.Redis.MaxActiveConns,
+	}
+
+	if config.Redis.DialTimeout != nil && *config.Redis.DialTimeout > 0 {
+		options.DialTimeout = time.Duration(*config.Redis.DialTimeout)
+	}
+
+	if config.Redis.ReadTimeout != nil {
+		if *config.Redis.ReadTimeout > 0 {
+			options.ReadTimeout = time.Duration(*config.Redis.ReadTimeout)
+		} else {
+			options.ReadTimeout = -1
+		}
+	}
+
+	if config.Redis.WriteTimeout != nil {
+		if *config.Redis.ReadTimeout > 0 {
+			options.WriteTimeout = time.Duration(*config.Redis.WriteTimeout)
+		} else {
+			options.WriteTimeout = -1
+		}
+	}
+
+	if config.Redis.TLS != nil {
+		var err error
+		options.TLSConfig, err = config.Redis.TLS.CreateTLSConfig(ctx)
+		if err != nil {
+			return nil, fmt.Errorf("creating TLS config: %w", err)
+		}
+	}
+
+	return &redisLimiter{
+		rate:     rate,
+		burst:    burst,
+		period:   config.Period,
+		maxDelay: maxDelay,
+		logger:   logger,
+		ttl:      ttl,
+		client:   redis.NewUniversalClient(options),
+	}, nil
+}
+
+func (r *redisLimiter) Allow(ctx context.Context, source string) (*time.Duration, error) {
+	ok, delay, err := r.evaluateScript(ctx, source)
+	if err != nil {
+		return nil, fmt.Errorf("evaluating script: %w", err)
+	}
+	if !ok {
+		return nil, nil
+	}
+	return delay, nil
+}
+
+func (r *redisLimiter) evaluateScript(ctx context.Context, key string) (bool, *time.Duration, error) {
+	if r.rate == rate.Inf {
+		return true, nil, nil
+	}
+
+	params := []interface{}{
+		float64(r.rate / 1000000),
+		r.burst,
+		r.ttl,
+		time.Now().UnixMicro(),
+		r.maxDelay.Microseconds(),
+	}
+	v, err := AllowTokenBucketScript.Run(ctx, r.client, []string{redisPrefix + key}, params...).Result()
+	if err != nil {
+		return false, nil, fmt.Errorf("running script: %w", err)
+	}
+
+	values := v.([]interface{})
+	ok, err := strconv.ParseBool(values[0].(string))
+	if err != nil {
+		return false, nil, fmt.Errorf("parsing ok value from redis rate lua script: %w", err)
+	}
+	delay, err := strconv.ParseFloat(values[1].(string), 64)
+	if err != nil {
+		return false, nil, fmt.Errorf("parsing delay value from redis rate lua script: %w", err)
+	}
+
+	microDelay := time.Duration(delay * float64(time.Microsecond))
+	return ok, &microDelay, nil
+}
diff --git a/pkg/provider/kubernetes/crd/fixtures/with_auth.yml b/pkg/provider/kubernetes/crd/fixtures/with_auth.yml
index 16d167e6ce..4a6c16e138 100644
--- a/pkg/provider/kubernetes/crd/fixtures/with_auth.yml
+++ b/pkg/provider/kubernetes/crd/fixtures/with_auth.yml
@@ -8,6 +8,7 @@ data:
   users: |2
     dGVzdDokYXByMSRINnVza2trVyRJZ1hMUDZld1RyU3VCa1RycUU4d2ovCnRlc3QyOiRhcHIxJGQ5
     aHI5SEJCJDRIeHdnVWlyM0hQNEVzZ2dQL1FObzAK
+
 ---
 apiVersion: v1
 kind: Secret
diff --git a/pkg/provider/kubernetes/crd/fixtures/with_ratelimit.yml b/pkg/provider/kubernetes/crd/fixtures/with_ratelimit.yml
new file mode 100644
index 0000000000..0ada06322f
--- /dev/null
+++ b/pkg/provider/kubernetes/crd/fixtures/with_ratelimit.yml
@@ -0,0 +1,82 @@
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+  name: ratelimit
+  namespace: default
+
+spec:
+  rateLimit:
+    period: 1m
+    average: 6
+    burst: 12
+    sourceCriterion:
+      ipStrategy:
+        excludedIPs:
+          - 127.0.0.1/32
+          - 192.168.1.7
+
+    redis:
+      secret: redissecret
+      endpoints:
+        - "127.0.0.1:6379"
+      tls:
+        certSecret: tlssecret
+        caSecret: casecret
+      db: 0
+      poolSize: 42
+      maxActiveConns: 42
+      readTimeout: 42s
+      writeTimeout: 42s
+      dialTimeout: 42s
+
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: redissecret
+  namespace: default
+data:
+  username: dXNlcg== # username: user
+  password: cGFzc3dvcmQ= # password: password
+
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: casecret
+  namespace: default
+
+data:
+  ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
+
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: tlssecret
+  namespace: default
+
+data:
+  tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
+  tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=
+
+---
+apiVersion: traefik.io/v1alpha1
+kind: IngressRoute
+metadata:
+  name: test2.route
+  namespace: default
+
+spec:
+  entryPoints:
+    - web
+
+  routes:
+    - match: Host(`foo.com`) && PathPrefix(`/will-be-limited`)
+      priority: 12
+      kind: Rule
+      services:
+        - name: whoami
+          port: 80
+      middlewares:
+        - name: ratelimit
diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go
index 5e435346bb..f9a8809c13 100644
--- a/pkg/provider/kubernetes/crd/kubernetes.go
+++ b/pkg/provider/kubernetes/crd/kubernetes.go
@@ -266,7 +266,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
 			continue
 		}
 
-		rateLimit, err := createRateLimitMiddleware(middleware.Spec.RateLimit)
+		rateLimit, err := createRateLimitMiddleware(client, middleware.Namespace, middleware.Spec.RateLimit)
 		if err != nil {
 			logger.Error().Err(err).Msg("Error while reading rateLimit middleware")
 			continue
@@ -686,7 +686,7 @@ func createCompressMiddleware(compress *traefikv1alpha1.Compress) *dynamic.Compr
 	return c
 }
 
-func createRateLimitMiddleware(rateLimit *traefikv1alpha1.RateLimit) (*dynamic.RateLimit, error) {
+func createRateLimitMiddleware(client Client, namespace string, rateLimit *traefikv1alpha1.RateLimit) (*dynamic.RateLimit, error) {
 	if rateLimit == nil {
 		return nil, nil
 	}
@@ -713,9 +713,97 @@ func createRateLimitMiddleware(rateLimit *traefikv1alpha1.RateLimit) (*dynamic.R
 		rl.SourceCriterion = rateLimit.SourceCriterion
 	}
 
+	if rateLimit.Redis != nil {
+		rl.Redis = &dynamic.Redis{
+			DB:             rateLimit.Redis.DB,
+			PoolSize:       rateLimit.Redis.PoolSize,
+			MinIdleConns:   rateLimit.Redis.MinIdleConns,
+			MaxActiveConns: rateLimit.Redis.MaxActiveConns,
+		}
+		rl.Redis.SetDefaults()
+
+		if len(rateLimit.Redis.Endpoints) > 0 {
+			rl.Redis.Endpoints = rateLimit.Redis.Endpoints
+		}
+
+		if rateLimit.Redis.TLS != nil {
+			rl.Redis.TLS = &types.ClientTLS{
+				InsecureSkipVerify: rateLimit.Redis.TLS.InsecureSkipVerify,
+			}
+
+			if len(rateLimit.Redis.TLS.CASecret) > 0 {
+				caSecret, err := loadCASecret(namespace, rateLimit.Redis.TLS.CASecret, client)
+				if err != nil {
+					return nil, fmt.Errorf("failed to load auth ca secret: %w", err)
+				}
+				rl.Redis.TLS.CA = caSecret
+			}
+
+			if len(rateLimit.Redis.TLS.CertSecret) > 0 {
+				authSecretCert, authSecretKey, err := loadAuthTLSSecret(namespace, rateLimit.Redis.TLS.CertSecret, client)
+				if err != nil {
+					return nil, fmt.Errorf("failed to load auth secret: %w", err)
+				}
+				rl.Redis.TLS.Cert = authSecretCert
+				rl.Redis.TLS.Key = authSecretKey
+			}
+		}
+
+		if rateLimit.Redis.DialTimeout != nil {
+			err := rl.Redis.DialTimeout.Set(rateLimit.Redis.DialTimeout.String())
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		if rateLimit.Redis.ReadTimeout != nil {
+			err := rl.Redis.ReadTimeout.Set(rateLimit.Redis.ReadTimeout.String())
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		if rateLimit.Redis.WriteTimeout != nil {
+			err := rl.Redis.WriteTimeout.Set(rateLimit.Redis.WriteTimeout.String())
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		if rateLimit.Redis.Secret != "" {
+			var err error
+			rl.Redis.Username, rl.Redis.Password, err = loadRedisCredentials(namespace, rateLimit.Redis.Secret, client)
+			if err != nil {
+				return nil, err
+			}
+		}
+	}
+
 	return rl, nil
 }
 
+func loadRedisCredentials(namespace, secretName string, k8sClient Client) (string, string, error) {
+	secret, exists, err := k8sClient.GetSecret(namespace, secretName)
+	if err != nil {
+		return "", "", fmt.Errorf("failed to fetch secret '%s/%s': %w", namespace, secretName, err)
+	}
+
+	if !exists {
+		return "", "", fmt.Errorf("secret '%s/%s' not found", namespace, secretName)
+	}
+
+	if secret == nil {
+		return "", "", fmt.Errorf("data for secret '%s/%s' must not be nil", namespace, secretName)
+	}
+
+	username, usernameExists := secret.Data["username"]
+	password, passwordExists := secret.Data["password"]
+	if !usernameExists || !passwordExists {
+		return "", "", fmt.Errorf("secret '%s/%s' must contain both username and password keys", secret.Namespace, secret.Name)
+	}
+	return string(username), string(password), nil
+}
+
 func createRetryMiddleware(retry *traefikv1alpha1.Retry) (*dynamic.Retry, error) {
 	if retry == nil {
 		return nil, nil
diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go
index 29810587f9..91d2fd9621 100644
--- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go
+++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/middleware.go
@@ -170,7 +170,7 @@ type ForwardAuth struct {
 	// If not set or empty then all request headers are passed.
 	AuthRequestHeaders []string `json:"authRequestHeaders,omitempty"`
 	// TLS defines the configuration used to secure the connection to the authentication server.
-	TLS *ClientTLS `json:"tls,omitempty"`
+	TLS *ClientTLSWithCAOptional `json:"tls,omitempty"`
 	// AddAuthCookiesToResponse defines the list of cookies to copy from the authentication server response to the response.
 	AddAuthCookiesToResponse []string `json:"addAuthCookiesToResponse,omitempty"`
 	// HeaderField defines a header field to store the authenticated user.
@@ -186,16 +186,12 @@ type ForwardAuth struct {
 	PreserveRequestMethod bool `json:"preserveRequestMethod,omitempty"`
 }
 
-// ClientTLS holds the client TLS configuration.
-type ClientTLS struct {
-	// CASecret is the name of the referenced Kubernetes Secret containing the CA to validate the server certificate.
-	// The CA certificate is extracted from key `tls.ca` or `ca.crt`.
-	CASecret string `json:"caSecret,omitempty"`
-	// CertSecret is the name of the referenced Kubernetes Secret containing the client certificate.
-	// The client certificate is extracted from the keys `tls.crt` and `tls.key`.
-	CertSecret string `json:"certSecret,omitempty"`
-	// InsecureSkipVerify defines whether the server certificates should be validated.
-	InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"`
+// +k8s:deepcopy-gen=true
+
+// ClientTLSWithCAOptional holds the client TLS configuration.
+// TODO: This has to be removed once the CAOptional option is removed.
+type ClientTLSWithCAOptional struct {
+	ClientTLS `json:",inline"`
 
 	// Deprecated: TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634).
 	CAOptional *bool `json:"caOptional,omitempty"`
@@ -225,6 +221,65 @@ type RateLimit struct {
 	// If several strategies are defined at the same time, an error will be raised.
 	// If none are set, the default is to use the request's remote address field (as an ipStrategy).
 	SourceCriterion *dynamic.SourceCriterion `json:"sourceCriterion,omitempty"`
+	// Redis hold the configs of Redis as bucket in rate limiter.
+	Redis *Redis `json:"redis,omitempty"`
+}
+
+// +k8s:deepcopy-gen=true
+
+// Redis contains the configuration for using Redis in middleware.
+// In a Kubernetes setup, the username and password are stored in a Secret file within the same namespace as the middleware.
+type Redis struct {
+	// Endpoints contains either a single address or a seed list of host:port addresses.
+	// Default value is ["localhost:6379"].
+	Endpoints []string `json:"endpoints,omitempty"`
+	// TLS defines TLS-specific configurations, including the CA, certificate, and key,
+	// which can be provided as a file path or file content.
+	TLS *ClientTLS `json:"tls,omitempty"`
+	// Secret defines the name of the referenced Kubernetes Secret containing Redis credentials.
+	Secret string `json:"secret,omitempty"`
+	// DB defines the Redis database that will be selected after connecting to the server.
+	DB int `json:"db,omitempty"`
+	// PoolSize defines the initial number of socket connections.
+	// If the pool runs out of available connections, additional ones will be created beyond PoolSize.
+	// This can be limited using MaxActiveConns.
+	// // Default value is 0, meaning 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
+	PoolSize int `json:"poolSize,omitempty"`
+	// MinIdleConns defines the minimum number of idle connections.
+	// Default value is 0, and idle connections are not closed by default.
+	MinIdleConns int `json:"minIdleConns,omitempty"`
+	// MaxActiveConns defines the maximum number of connections allocated by the pool at a given time.
+	// Default value is 0, meaning there is no limit.
+	MaxActiveConns int `json:"maxActiveConns,omitempty"`
+	// ReadTimeout defines the timeout for socket read operations.
+	// Default value is 3 seconds.
+	// +kubebuilder:validation:Pattern="^([0-9]+(ns|us|µs|ms|s|m|h)?)+$"
+	// +kubebuilder:validation:XIntOrString
+	ReadTimeout *intstr.IntOrString `json:"readTimeout,omitempty"`
+	// WriteTimeout defines the timeout for socket write operations.
+	// Default value is 3 seconds.
+	// +kubebuilder:validation:Pattern="^([0-9]+(ns|us|µs|ms|s|m|h)?)+$"
+	// +kubebuilder:validation:XIntOrString
+	WriteTimeout *intstr.IntOrString `json:"writeTimeout,omitempty"`
+	// DialTimeout sets the timeout for establishing new connections.
+	// Default value is 5 seconds.
+	// +kubebuilder:validation:Pattern="^([0-9]+(ns|us|µs|ms|s|m|h)?)+$"
+	// +kubebuilder:validation:XIntOrString
+	DialTimeout *intstr.IntOrString `json:"dialTimeout,omitempty"`
+}
+
+// +k8s:deepcopy-gen=true
+
+// ClientTLS holds the client TLS configuration.
+type ClientTLS struct {
+	// CASecret is the name of the referenced Kubernetes Secret containing the CA to validate the server certificate.
+	// The CA certificate is extracted from key `tls.ca` or `ca.crt`.
+	CASecret string `json:"caSecret,omitempty"`
+	// CertSecret is the name of the referenced Kubernetes Secret containing the client certificate.
+	// The client certificate is extracted from the keys `tls.crt` and `tls.key`.
+	CertSecret string `json:"certSecret,omitempty"`
+	// InsecureSkipVerify defines whether the server certificates should be validated.
+	InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"`
 }
 
 // +k8s:deepcopy-gen=true
diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go
index ae83e6173d..e9dd2386ba 100644
--- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go
@@ -146,6 +146,23 @@ func (in *ClientAuth) DeepCopy() *ClientAuth {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ClientTLS) DeepCopyInto(out *ClientTLS) {
 	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientTLS.
+func (in *ClientTLS) DeepCopy() *ClientTLS {
+	if in == nil {
+		return nil
+	}
+	out := new(ClientTLS)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ClientTLSWithCAOptional) DeepCopyInto(out *ClientTLSWithCAOptional) {
+	*out = *in
+	out.ClientTLS = in.ClientTLS
 	if in.CAOptional != nil {
 		in, out := &in.CAOptional, &out.CAOptional
 		*out = new(bool)
@@ -154,12 +171,12 @@ func (in *ClientTLS) DeepCopyInto(out *ClientTLS) {
 	return
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientTLS.
-func (in *ClientTLS) DeepCopy() *ClientTLS {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientTLSWithCAOptional.
+func (in *ClientTLSWithCAOptional) DeepCopy() *ClientTLSWithCAOptional {
 	if in == nil {
 		return nil
 	}
-	out := new(ClientTLS)
+	out := new(ClientTLSWithCAOptional)
 	in.DeepCopyInto(out)
 	return out
 }
@@ -265,7 +282,7 @@ func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) {
 	}
 	if in.TLS != nil {
 		in, out := &in.TLS, &out.TLS
-		*out = new(ClientTLS)
+		*out = new(ClientTLSWithCAOptional)
 		(*in).DeepCopyInto(*out)
 	}
 	if in.AddAuthCookiesToResponse != nil {
@@ -1053,6 +1070,11 @@ func (in *RateLimit) DeepCopyInto(out *RateLimit) {
 		*out = new(dynamic.SourceCriterion)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Redis != nil {
+		in, out := &in.Redis, &out.Redis
+		*out = new(Redis)
+		(*in).DeepCopyInto(*out)
+	}
 	return
 }
 
@@ -1066,6 +1088,47 @@ func (in *RateLimit) DeepCopy() *RateLimit {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Redis) DeepCopyInto(out *Redis) {
+	*out = *in
+	if in.Endpoints != nil {
+		in, out := &in.Endpoints, &out.Endpoints
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.TLS != nil {
+		in, out := &in.TLS, &out.TLS
+		*out = new(ClientTLS)
+		**out = **in
+	}
+	if in.ReadTimeout != nil {
+		in, out := &in.ReadTimeout, &out.ReadTimeout
+		*out = new(intstr.IntOrString)
+		**out = **in
+	}
+	if in.WriteTimeout != nil {
+		in, out := &in.WriteTimeout, &out.WriteTimeout
+		*out = new(intstr.IntOrString)
+		**out = **in
+	}
+	if in.DialTimeout != nil {
+		in, out := &in.DialTimeout, &out.DialTimeout
+		*out = new(intstr.IntOrString)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Redis.
+func (in *Redis) DeepCopy() *Redis {
+	if in == nil {
+		return nil
+	}
+	out := new(Redis)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ResponseForwarding) DeepCopyInto(out *ResponseForwarding) {
 	*out = *in


Test patch:
diff --git a/integration/ratelimit_test.go b/integration/ratelimit_test.go
index b0254564dc..c0f0c96b32 100644
--- a/integration/ratelimit_test.go
+++ b/integration/ratelimit_test.go
@@ -1,6 +1,7 @@
 package integration
 
 import (
+	"net"
 	"net/http"
 	"testing"
 	"time"
@@ -12,7 +13,8 @@ import (
 
 type RateLimitSuite struct {
 	BaseSuite
-	ServerIP string
+	ServerIP      string
+	RedisEndpoint string
 }
 
 func TestRateLimitSuite(t *testing.T) {
@@ -26,6 +28,7 @@ func (s *RateLimitSuite) SetupSuite() {
 	s.composeUp()
 
 	s.ServerIP = s.getComposeServiceIP("whoami1")
+	s.RedisEndpoint = net.JoinHostPort(s.getComposeServiceIP("redis"), "6379")
 }
 
 func (s *RateLimitSuite) TearDownSuite() {
@@ -58,3 +61,34 @@ func (s *RateLimitSuite) TestSimpleConfiguration() {
 		s.T().Fatalf("requests throughput was too fast wrt to rate limiting: 100 requests in %v", elapsed)
 	}
 }
+
+func (s *RateLimitSuite) TestRedisRateLimitSimpleConfiguration() {
+	file := s.adaptFile("fixtures/ratelimit/simple_redis.toml", struct {
+		Server1       string
+		RedisEndpoint string
+	}{
+		Server1:       s.ServerIP,
+		RedisEndpoint: s.RedisEndpoint,
+	})
+
+	s.traefikCmd(withConfigFile(file))
+
+	err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("ratelimit", "redis"))
+	require.NoError(s.T(), err)
+
+	start := time.Now()
+	count := 0
+	for {
+		err = try.GetRequest("http://127.0.0.1:8081/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK))
+		require.NoError(s.T(), err)
+		count++
+		if count > 100 {
+			break
+		}
+	}
+	stop := time.Now()
+	elapsed := stop.Sub(start)
+	if elapsed < time.Second*99/100 {
+		s.T().Fatalf("requests throughput was too fast wrt to rate limiting: 100 requests in %v", elapsed)
+	}
+}
diff --git a/pkg/middlewares/ratelimiter/rate_limiter_test.go b/pkg/middlewares/ratelimiter/rate_limiter_test.go
index d61e4243cc..b16dccf004 100644
--- a/pkg/middlewares/ratelimiter/rate_limiter_test.go
+++ b/pkg/middlewares/ratelimiter/rate_limiter_test.go
@@ -2,19 +2,25 @@ package ratelimiter
 
 import (
 	"context"
+	"errors"
 	"fmt"
+	"math/rand"
 	"net/http"
 	"net/http/httptest"
 	"os"
+	"strconv"
 	"testing"
 	"time"
 
+	"github.com/mailgun/ttlmap"
+	"github.com/redis/go-redis/v9"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	ptypes "github.com/traefik/paerser/types"
 	"github.com/traefik/traefik/v3/pkg/config/dynamic"
 	"github.com/traefik/traefik/v3/pkg/testhelpers"
 	"github.com/vulcand/oxy/v2/utils"
+	lua "github.com/yuin/gopher-lua"
 	"golang.org/x/time/rate"
 )
 
@@ -84,7 +90,17 @@ func TestNewRateLimiter(t *testing.T) {
 					RequestHeaderName: "Foo",
 				},
 			},
-			expectedError: "iPStrategy and RequestHeaderName are mutually exclusive",
+			expectedError: "getting source extractor: iPStrategy and RequestHeaderName are mutually exclusive",
+		},
+		{
+			desc: "Use Redis",
+			config: dynamic.RateLimit{
+				Average: 200,
+				Burst:   10,
+				Redis: &dynamic.Redis{
+					Endpoints: []string{"localhost:6379"},
+				},
+			},
 		},
 	}
 
@@ -138,7 +154,7 @@ func TestNewRateLimiter(t *testing.T) {
 	}
 }
 
-func TestRateLimit(t *testing.T) {
+func TestInMemoryRateLimit(t *testing.T) {
 	testCases := []struct {
 		desc         string
 		config       dynamic.RateLimit
@@ -326,15 +342,357 @@ func TestRateLimit(t *testing.T) {
 			minCount := computeMinCount(wantCount)
 
 			if reqCount < minCount {
-				t.Fatalf("rate was slower than expected: %d requests (wanted > %d) in %v", reqCount, minCount, elapsed)
+				t.Fatalf("rate was slower than expected: %d requests (wanted > %d) (dropped %d)  in %v", reqCount, minCount, dropped, elapsed)
 			}
 			if reqCount > maxCount {
-				t.Fatalf("rate was faster than expected: %d requests (wanted < %d) in %v", reqCount, maxCount, elapsed)
+				t.Fatalf("rate was faster than expected: %d requests (wanted < %d) (dropped %d) in %v", reqCount, maxCount, dropped, elapsed)
 			}
 		})
 	}
 }
 
+func TestRedisRateLimit(t *testing.T) {
+	testCases := []struct {
+		desc         string
+		config       dynamic.RateLimit
+		loadDuration time.Duration
+		incomingLoad int // in reqs/s
+		burst        int
+	}{
+		{
+			desc: "Average is respected",
+			config: dynamic.RateLimit{
+				Average: 100,
+				Burst:   1,
+			},
+			loadDuration: 2 * time.Second,
+			incomingLoad: 400,
+		},
+		{
+			desc: "burst allowed, no bursty traffic",
+			config: dynamic.RateLimit{
+				Average: 100,
+				Burst:   100,
+			},
+			loadDuration: 2 * time.Second,
+			incomingLoad: 200,
+		},
+		{
+			desc: "burst allowed, initial burst, under capacity",
+			config: dynamic.RateLimit{
+				Average: 100,
+				Burst:   100,
+			},
+			loadDuration: 2 * time.Second,
+			incomingLoad: 200,
+			burst:        50,
+		},
+		{
+			desc: "burst allowed, initial burst, over capacity",
+			config: dynamic.RateLimit{
+				Average: 100,
+				Burst:   100,
+			},
+			loadDuration: 2 * time.Second,
+			incomingLoad: 200,
+			burst:        150,
+		},
+		{
+			desc: "burst over average, initial burst, over capacity",
+			config: dynamic.RateLimit{
+				Average: 100,
+				Burst:   200,
+			},
+			loadDuration: 2 * time.Second,
+			incomingLoad: 200,
+			burst:        300,
+		},
+		{
+			desc: "lower than 1/s",
+			config: dynamic.RateLimit{
+				// Bug on gopher-lua on parsing the string to number "5e-07" => 0.0000005
+				// See https://github.com/yuin/gopher-lua/issues/491
+				// Average: 5,
+				Average: 1,
+				Period:  ptypes.Duration(10 * time.Second),
+			},
+			loadDuration: 2 * time.Second,
+			incomingLoad: 100,
+			burst:        0,
+		},
+		{
+			desc: "lower than 1/s, longer",
+			config: dynamic.RateLimit{
+				// Bug on gopher-lua on parsing the string to number "5e-07" => 0.0000005
+				// See https://github.com/yuin/gopher-lua/issues/491
+				// Average: 5,
+				Average: 1,
+				Period:  ptypes.Duration(10 * time.Second),
+			},
+			loadDuration: time.Minute,
+			incomingLoad: 100,
+			burst:        0,
+		},
+		{
+			desc: "lower than 1/s, longer, harsher",
+			config: dynamic.RateLimit{
+				Average: 1,
+				Period:  ptypes.Duration(time.Minute),
+			},
+			loadDuration: time.Minute,
+			incomingLoad: 100,
+			burst:        0,
+		},
+		{
+			desc: "period below 1 second",
+			config: dynamic.RateLimit{
+				Average: 50,
+				Period:  ptypes.Duration(500 * time.Millisecond),
+			},
+			loadDuration: 2 * time.Second,
+			incomingLoad: 300,
+			burst:        0,
+		},
+		// TODO Try to disambiguate when it fails if it is because of too high a load.
+		// {
+		// 	desc: "Zero average ==> no rate limiting",
+		// 	config: dynamic.RateLimit{
+		// 		Average: 0,
+		// 		Burst:   1,
+		// 	},
+		// 	incomingLoad: 1000,
+		// 	loadDuration: time.Second,
+		// },
+	}
+
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			randPort := rand.Int()
+			if test.loadDuration >= time.Minute && testing.Short() {
+				t.Skip("skipping test in short mode.")
+			}
+			t.Parallel()
+
+			reqCount := 0
+			dropped := 0
+			next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				reqCount++
+			})
+			test.config.Redis = &dynamic.Redis{
+				Endpoints: []string{"localhost:6379"},
+			}
+			h, err := New(context.Background(), next, test.config, "rate-limiter")
+			require.NoError(t, err)
+
+			l := h.(*rateLimiter)
+
+			limiter := l.limiter.(*redisLimiter)
+			limiter.client = newMockRedisClient(limiter.ttl)
+
+			h = l
+
+			loadPeriod := time.Duration(1e9 / test.incomingLoad)
+			start := time.Now()
+			end := start.Add(test.loadDuration)
+			ticker := time.NewTicker(loadPeriod)
+			defer ticker.Stop()
+			for {
+				if time.Now().After(end) {
+					break
+				}
+
+				req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil)
+				req.RemoteAddr = "127.0.0." + strconv.Itoa(randPort) + ":" + strconv.Itoa(randPort)
+				w := httptest.NewRecorder()
+
+				h.ServeHTTP(w, req)
+				if w.Result().StatusCode != http.StatusOK {
+					dropped++
+				}
+				if test.burst > 0 && reqCount < test.burst {
+					// if a burst is defined we first hammer the server with test.burst requests as fast as possible
+					continue
+				}
+				<-ticker.C
+			}
+			stop := time.Now()
+			elapsed := stop.Sub(start)
+
+			burst := test.config.Burst
+			if burst < 1 {
+				// actual default value
+				burst = 1
+			}
+
+			period := time.Duration(test.config.Period)
+			if period == 0 {
+				period = time.Second
+			}
+
+			if test.config.Average == 0 {
+				if reqCount < 75*test.incomingLoad/100 {
+					t.Fatalf("we (arbitrarily) expect at least 75%% of the requests to go through with no rate limiting, and yet only %d/%d went through", reqCount, test.incomingLoad)
+				}
+				if dropped != 0 {
+					t.Fatalf("no request should have been dropped if rate limiting is disabled, and yet %d were", dropped)
+				}
+				return
+			}
+
+			// Note that even when there is no bursty traffic,
+			// we take into account the configured burst,
+			// because it also helps absorbing non-bursty traffic.
+			rate := float64(test.config.Average) / float64(period)
+
+			wantCount := int(int64(rate*float64(test.loadDuration)) + burst)
+
+			// Allow for a 2% leeway
+			maxCount := wantCount * 102 / 100
+
+			// With very high CPU loads,
+			// we can expect some extra delay in addition to the rate limiting we already do,
+			// so we allow for some extra leeway there.
+			// Feel free to adjust wrt to the load on e.g. the CI.
+			minCount := computeMinCount(wantCount)
+
+			if reqCount < minCount {
+				t.Fatalf("rate was slower than expected: %d requests (wanted > %d) (dropped %d)  in %v", reqCount, minCount, dropped, elapsed)
+			}
+			if reqCount > maxCount {
+				t.Fatalf("rate was faster than expected: %d requests (wanted < %d) (dropped %d) in %v", reqCount, maxCount, dropped, elapsed)
+			}
+		})
+	}
+}
+
+type mockRedisClient struct {
+	ttl  int
+	keys *ttlmap.TtlMap
+}
+
+func newMockRedisClient(ttl int) Rediser {
+	buckets, _ := ttlmap.NewConcurrent(65536)
+	return &mockRedisClient{
+		ttl:  ttl,
+		keys: buckets,
+	}
+}
+
+func (m *mockRedisClient) EvalSha(ctx context.Context, _ string, keys []string, args ...interface{}) *redis.Cmd {
+	state := lua.NewState()
+	defer state.Close()
+
+	tableKeys := state.NewTable()
+	for _, key := range keys {
+		tableKeys.Append(lua.LString(key))
+	}
+	state.SetGlobal("KEYS", tableKeys)
+
+	tableArgv := state.NewTable()
+	for _, arg := range args {
+		tableArgv.Append(lua.LString(fmt.Sprint(arg)))
+	}
+	state.SetGlobal("ARGV", tableArgv)
+
+	mod := state.SetFuncs(state.NewTable(), map[string]lua.LGFunction{
+		"call": func(state *lua.LState) int {
+			switch state.Get(1).String() {
+			case "hset":
+				key := state.Get(2).String()
+				keyLast := state.Get(3).String()
+				last := state.Get(4).String()
+				keyTokens := state.Get(5).String()
+				tokens := state.Get(6).String()
+				table := []string{keyLast, last, keyTokens, tokens}
+				_ = m.keys.Set(key, table, m.ttl)
+			case "hgetall":
+				key := state.Get(2).String()
+				value, ok := m.keys.Get(key)
+				table := state.NewTable()
+				if !ok {
+					state.Push(table)
+				} else {
+					switch v := value.(type) {
+					case []string:
+						if len(v) != 4 {
+							break
+						}
+						for i := range v {
+							table.Append(lua.LString(v[i]))
+						}
+					default:
+						fmt.Printf("Unknown type: %T\n", v)
+					}
+					state.Push(table)
+				}
+			case "expire":
+			default:
+				return 0
+			}
+
+			return 1
+		},
+	})
+	state.SetGlobal("redis", mod)
+	state.Push(mod)
+
+	cmd := redis.NewCmd(ctx)
+	if err := state.DoString(AllowTokenBucketRaw); err != nil {
+		cmd.SetErr(err)
+		return cmd
+	}
+
+	result := state.Get(2)
+	resultTable, ok := result.(*lua.LTable)
+	if !ok {
+		cmd.SetErr(errors.New("unexpected response type: " + result.String()))
+		return cmd
+	}
+
+	var resultSlice []interface{}
+	resultTable.ForEach(func(_ lua.LValue, value lua.LValue) {
+		valueNbr, ok := value.(lua.LNumber)
+		if !ok {
+			valueStr, ok := value.(lua.LString)
+			if !ok {
+				cmd.SetErr(errors.New("unexpected response value type " + value.String()))
+			}
+			resultSlice = append(resultSlice, string(valueStr))
+			return
+		}
+
+		resultSlice = append(resultSlice, int64(valueNbr))
+	})
+
+	cmd.SetVal(resultSlice)
+
+	return cmd
+}
+
+func (m *mockRedisClient) Eval(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd {
+	return m.EvalSha(ctx, script, keys, args...)
+}
+
+func (m *mockRedisClient) ScriptExists(ctx context.Context, hashes ...string) *redis.BoolSliceCmd {
+	return nil
+}
+
+func (m *mockRedisClient) ScriptLoad(ctx context.Context, script string) *redis.StringCmd {
+	return nil
+}
+
+func (m *mockRedisClient) Del(ctx context.Context, keys ...string) *redis.IntCmd {
+	return nil
+}
+
+func (m *mockRedisClient) EvalRO(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd {
+	return nil
+}
+
+func (m *mockRedisClient) EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd {
+	return nil
+}
+
 func computeMinCount(wantCount int) int {
 	if os.Getenv("CI") != "" {
 		return wantCount * 60 / 100
diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go
index 3d88287eab..70b4779dca 100644
--- a/pkg/provider/kubernetes/crd/kubernetes_test.go
+++ b/pkg/provider/kubernetes/crd/kubernetes_test.go
@@ -1835,6 +1835,84 @@ func TestLoadIngressRoutes(t *testing.T) {
 				TLS: &dynamic.TLSConfiguration{},
 			},
 		},
+		{
+			desc:                "Simple Ingress Route with middleware ratelimit",
+			allowCrossNamespace: true,
+			paths:               []string{"services.yml", "with_ratelimit.yml"},
+			expected: &dynamic.Configuration{
+				UDP: &dynamic.UDPConfiguration{
+					Routers:  map[string]*dynamic.UDPRouter{},
+					Services: map[string]*dynamic.UDPService{},
+				},
+				TCP: &dynamic.TCPConfiguration{
+					Routers:           map[string]*dynamic.TCPRouter{},
+					Middlewares:       map[string]*dynamic.TCPMiddleware{},
+					Services:          map[string]*dynamic.TCPService{},
+					ServersTransports: map[string]*dynamic.TCPServersTransport{},
+				},
+				HTTP: &dynamic.HTTPConfiguration{
+					Routers: map[string]*dynamic.Router{
+						"default-test2-route-3c9bf014491ebdba74f7": {
+							EntryPoints: []string{"web"},
+							Service:     "default-test2-route-3c9bf014491ebdba74f7",
+							Rule:        "Host(`foo.com`) && PathPrefix(`/will-be-limited`)",
+							Priority:    12,
+							Middlewares: []string{"default-ratelimit"},
+						},
+					},
+					Middlewares: map[string]*dynamic.Middleware{
+						"default-ratelimit": {
+							RateLimit: &dynamic.RateLimit{
+								Average: 6,
+								Burst:   12,
+								Period:  ptypes.Duration(60 * time.Second),
+								SourceCriterion: &dynamic.SourceCriterion{
+									IPStrategy: &dynamic.IPStrategy{
+										ExcludedIPs: []string{"127.0.0.1/32", "192.168.1.7"},
+									},
+								},
+								Redis: &dynamic.Redis{
+									Endpoints: []string{"127.0.0.1:6379"},
+									Username:  "user",
+									Password:  "password",
+									TLS: &types.ClientTLS{
+										CA:   "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
+										Cert: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----",
+										Key:  "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----",
+									},
+									DB:             0,
+									PoolSize:       42,
+									MaxActiveConns: 42,
+									ReadTimeout:    pointer(ptypes.Duration(42 * time.Second)),
+									WriteTimeout:   pointer(ptypes.Duration(42 * time.Second)),
+									DialTimeout:    pointer(ptypes.Duration(42 * time.Second)),
+								},
+							},
+						},
+					},
+					Services: map[string]*dynamic.Service{
+						"default-test2-route-3c9bf014491ebdba74f7": {
+							LoadBalancer: &dynamic.ServersLoadBalancer{
+								Servers: []dynamic.Server{
+									{
+										URL: "http://10.10.0.1:80",
+									},
+									{
+										URL: "http://10.10.0.2:80",
+									},
+								},
+								PassHostHeader: pointer(true),
+								ResponseForwarding: &dynamic.ResponseForwarding{
+									FlushInterval: ptypes.Duration(100 * time.Millisecond),
+								},
+							},
+						},
+					},
+					ServersTransports: map[string]*dynamic.ServersTransport{},
+				},
+				TLS: &dynamic.TLSConfiguration{},
+			},
+		},
 		{
 			desc:                "Middlewares in ingress route config are normalized",
 			allowCrossNamespace: true,