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.
Written by
Technical Writer at Civo
Written by
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:
- kubectl installed
- A Civo account
- The Civo CLI installed and configured
- Basic familiarity with Kubernetes
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=Traefikkubectl 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/v1kind: Deploymentmetadata:name: whoaminamespace: defaultspec:replicas: 1selector:matchLabels:app: whoamitemplate:metadata:labels:app: whoamispec:containers:- name: whoamiimage: traefik/whoamiports:- containerPort: 80---apiVersion: v1kind: Servicemetadata:name: whoaminamespace: defaultspec:selector:app: whoamiports:- protocol: TCPport: 80targetPort: 80EOF
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
averageis measured against. Withaverage: 10andperiod: 1swe 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/v1alpha1kind: Middlewaremetadata:name: ratelimit-rpsnamespace: defaultspec:rateLimit:average: 10period: 1sburst: 5EOF
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/v1alpha1kind: Middlewaremetadata:name: inflight-connsnamespace: defaultspec:inFlightReq:amount: 5sourceCriterion:ipStrategy:depth: 1EOF
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.
Option A (recommended): plain Ingress plus annotation
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 - <<EOFapiVersion: networking.k8s.io/v1kind: Ingressmetadata:name: whoaminamespace: defaultannotations:traefik.ingress.kubernetes.io/router.middlewares: default-ratelimit-rps@kubernetescrdspec:ingressClassName: traefikrules:- host: "${HOST}"http:paths:- path: /pathType: Prefixbackend:service:name: whoamiport:number: 80EOF
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 - <<EOFapiVersion: traefik.io/v1alpha1kind: IngressRoutemetadata:name: whoaminamespace: defaultspec:entryPoints:- webroutes:- match: Host(\`${HOST}\`)kind: Ruleservices:- name: whoamiport: 80middlewares:- name: ratelimit-rps- name: inflight-connsEOF
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 %)
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.

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.
Share this article
Further Reading
17 February 2022
A/B testing using the NGINX Kubernetes Ingress controller
15 January 2024