Rate-limiting applications with Traefik on Civo

Learn how to implement rate-limiting using the NGINX Ingress controller in a Kubernetes environment. This tutorial covers the benefits of rate-limiting and a step-by-step guide on setting up rate-limiting.

5 minutes reading time

Written by

Jubril Oyetunji
Jubril Oyetunji

Technical Writer at Civo

Rate-limiting is a method of reducing the rate at which requests are made to a server or resource on a network. It plays a crucial role in preventing abuse and controlling traffic, whether that traffic is a scraper, a runaway retry loop, or a brute-force script.

One of the key advantages of enforcing rate limits at the ingress layer is that you don't need to introduce any additional logic or modifications to your application code. This decouples rate-limiting logic from your application, simplifies the deployment process, and enables you to manage rate limits centrally, regardless of the underlying application architecture.

The original version of this tutorial used the ingress-nginx controller. The Kubernetes project has since put ingress-nginx on the path to retirement, with end of life in March 2026 and no further releases, bugfixes, or security patches after that. On Civo you don't need it anyway: Civo k3s ships Traefik as the default ingress controller, so it is already running on every cluster you create. In this guide we'll use Traefik's native middleware to rate-limit a sample application.

Benefits of rate-limiting

Some of the benefits of rate-limiting include:

  • Abuse prevention: limiting the rate of requests helps deter brute-force attacks.
  • Denial-of-service (DoS) prevention: rate limits mitigate attacks that target your application's availability.
  • Cost optimization: when you are charged per API call, capping request rates keeps usage under control.

Prerequisites

To follow along you'll need:

Creating a cluster

We only need Traefik, and Civo k3s installs it by default, so the create command is short. There's no need to add a second controller:

civo k3s create --create-firewall --nodes 2 -m --save --switch --wait \
traefik-rate-test -r=Traefik
kubectl get nodes

Once the nodes report Ready, confirm Traefik's load balancer is up:

kubectl get svc -n kube-system traefik

Note on Traefik versions: A current Civo k3s cluster runs k3s v1.32 or newer, which ships Traefik v3. Traefik v3 uses the traefik.io/v1alpha1 API group, which is what every manifest below uses. If you are on an older cluster running Traefik v2, change the apiVersion to traefik.containo.us/v1alpha1. The fields are otherwise identical.

Deploying a sample application

We'll deploy traefik/whoami, a tiny service that echoes request details back to the caller. That makes the rate-limit behavior easy to observe. Apply the Deployment and Service:

kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: traefik/whoami
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: whoami
namespace: default
spec:
selector:
app: whoami
ports:
- protocol: TCP
port: 80
targetPort: 80
EOF

Grab the public hostname Traefik is serving on. We'll use it for every test:

export HOST=$(kubectl get svc -n kube-system traefik \
-o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
echo "Using HOST=$HOST"

Implementing rate-limiting (requests per second)

Instead of an ingress annotation, we'll define a Traefik RateLimit middleware. Traefik uses a token-bucket algorithm, so three fields control the behavior:

  • average: the steady rate of requests allowed, measured over period.
  • period: the window average is measured against. With average: 10 and period: 1s we allow an average of 10 requests per second.
  • burst: the bucket size, the number of requests allowed to spike through at once before the average kicks in.

Apply the middleware:

kubectl apply -f - <<'EOF'
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: ratelimit-rps
namespace: default
spec:
rateLimit:
average: 10
period: 1s
burst: 5
EOF

The 503 vs 429 difference: ingress-nginx answered an exceeded limit with 503 Service Unavailable. Traefik answers with 429 Too Many Requests, which is the semantically correct code for rate-limiting. If you have alerting or client retry logic keyed on the status code, update it from 503 to 429.

Rate-limiting by connections (in-flight requests)

Rate per second is one dimension. Another is how many requests a single client can have in flight at the same time. For that we use a Traefik inFlightReq middleware. The amount field caps simultaneous in-flight requests.

By default Traefik groups in-flight requests by request host, which would apply one shared limit to everyone. To make the limit per client IP, we add a sourceCriterion.ipStrategy:

kubectl apply -f - <<'EOF'
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: inflight-conns
namespace: default
spec:
inFlightReq:
amount: 5
sourceCriterion:
ipStrategy:
depth: 1
EOF

On ipStrategy: because traffic reaches whoami through the Civo load balancer and Traefik, the real client IP arrives in the X-Forwarded-For header. depth: 1 tells Traefik to take the first IP from the right of that header, which is the client as seen by the load balancer. If you instead set excludedIPs, Traefik walks the header skipping those addresses. Use depth for a fixed, predictable proxy chain.

Confirm both middleware exist:

kubectl get middleware -n default

Attaching the middleware

Defining a middleware doesn't do anything on its own. It has to be wired onto a route. There are two ways to do that. We'll show both and recommend the first for readers coming from a plain Ingress.

This keeps a standard, portable Ingress object and references the middleware through an annotation. The reference must be namespace-qualified in the form <namespace>-<middleware-name>@kubernetescrd: ratelimit-rps@kubernetescrd on its own will silently fail to attach, it has to be default-ratelimit-rps@kubernetescrd:

kubectl apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: whoami
namespace: default
annotations:
traefik.ingress.kubernetes.io/router.middlewares: default-ratelimit-rps@kubernetescrd
spec:
ingressClassName: traefik
rules:
- host: "${HOST}"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami
port:
number: 80
EOF

To chain both middleware, comma-separate them in the annotation:

traefik.ingress.kubernetes.io/router.middlewares: default-ratelimit-rps@kubernetescrd,default-inflight-conns@kubernetescrd

Option B: Traefik IngressRoute CR

IngressRoute is Traefik's native routing object. It references middleware by name and namespace directly, with no @kubernetescrd suffix:

kubectl apply -f - <<EOF
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: whoami
namespace: default
spec:
entryPoints:
- web
routes:
- match: Host(\`${HOST}\`)
kind: Rule
services:
- name: whoami
port: 80
middlewares:
- name: ratelimit-rps
- name: inflight-conns
EOF

Use Option A if you want to keep portable, standard Ingress objects and only annotate them. Use Option B if you are already all-in on Traefik CRs and want the matcher expressiveness of IngressRoute.

Load testing with fortio (expecting 429s)

To confirm the limit works, install fortio locally, then drive more traffic than the limit allows. With average: 10 per second, we'll push 15 qps:

fortio load --qps 15 -t 30s "http://${HOST}/"

Over the limit, fortio logs each rejected request as a 429 (not a 503), with lines like:

{"ts":1750000000.123456,"level":"warn","file":"http_client.go","line":1079,"msg":"Non ok http code","code":"429","status":"HTTP/1.1 429","thread":"2","run":"0"}

The end-of-run summary shows the split between accepted and rejected requests. This capture is from a live 50-request burst against the RateLimit middleware:

Code 200 : 16 (32.0 %)
Code 429 : 34 (68.0 %)
Load testing with fortio (expecting 429s)

A 50-request burst against the RateLimit middleware: a portion served 200, the rest rejected with 429.

A control test confirms the middleware is what's enforcing the limit: remove the annotation from the Ingress, re-run the burst, and every request returns 200 again.

To exercise the connection limit instead, push concurrent connections above amount: 5:

fortio load --qps 30 -t 30s -c 8 "http://${HOST}/"

Connections beyond 5 in flight at once are rejected with 429.

Cleaning up

When you're done, remove the cluster so you stop paying for it:

civo k3s remove traefik-rate-test

Summary

Rate-limiting can be extremely useful in mitigating DoS attacks and preventing abuse. In this guide we used the Traefik ingress controller that ships with Civo k3s, defined a RateLimit middleware for requests per second and an inFlightReq middleware for concurrent connections, attached them with either a plain annotated Ingress or a native IngressRoute, and confirmed with fortio that traffic over the limit is rejected with 429 Too Many Requests.

Before implementing rate-limiting, it is crucial to assess your environment and requirements, considering expected traffic patterns, application sensitivity, and potential security risks. A limit that is too aggressive will reject legitimate users. From here you can tune average, burst, and amount to match your service's capacity, chain additional middleware (retries, circuit breakers, headers), and explore the full RateLimit and inFlightReq references.

Jubril Oyetunji
Jubril Oyetunji

Technical Writer at Civo

Jubril Oyetunji is a DevOps engineer and technical writer with a strong focus on cloud-native technologies and open-source tools. His work centers on creating practical tutorials that help developers better understand platforms such as Kubernetes, NGINX, Rust, and Go.

As a contract technical writer, Jubril authored an extensive library of technical guides covering cloud-native infrastructure and modern development workflows. Many of his tutorials achieved strong search rankings, helping developers around the world learn and adopt emerging technologies.

View author profile