This episode covers rate limiting in Fiber v3: the limiter middleware with Max and Expiration, an IP-based KeyGenerator, handling LimitReached with 429, the sliding window algorithm, and Redis storage for a limiter shared across instances.

A public API without protection is easily flooded — whether by bots, brute-force attempts, or a client bug. Episode 20 covers rate limiting in Fiber v3: limiting the number of requests per client within a time period, customizing the key by IP, and sharing limiter state across instances with Redis.
Rate limiting isn't just about security. It also keeps infrastructure costs under control and protects other users from service degradation. That's why almost every production API uses a limiter at the outermost layer.
limiter.New from middleware/limiter limits the number of requests:
import "github.com/gofiber/fiber/v3/middleware/limiter"
app.Use(limiter.New(limiter.Config{
Max: 20,
Expiration: 30 * time.Second,
}))Max: 20 allows 20 requests per key within Expiration: 30s. Past the limit, requests are rejected. Without a KeyGenerator, the default key is the client's IP address. That's enough for a prototype, but needs adjustment when running behind a proxy.
Behind a reverse proxy, the client IP is in the X-Forwarded-For header. Set KeyGenerator to read from the right source:
app.Use(limiter.New(limiter.Config{
Max: 20,
Expiration: 30 * time.Second,
KeyGenerator: func(c fiber.Ctx) string {
if ip := c.Get("X-Forwarded-For"); ip != "" {
return ip
}
return c.IP()
},
}))KeyGenerator determines the client identity. This example uses X-Forwarded-For when available, falling back to c.IP(). That way every user behind a single proxy IP is counted separately, and one client can't drain another client's quota.
When the limit is exceeded, LimitReached is called. Customize it for an informative response:
app.Use(limiter.New(limiter.Config{
Max: 20,
Expiration: 30 * time.Second,
LimitReached: func(c fiber.Ctx) error {
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
"error": "terlalu banyak request, coba lagi nanti",
})
},
}))LimitReached replaces the default 429 response with a clear JSON body. Add a Retry-After header if you want to tell clients when they may try again. Consistent responses make it easy for clients to handle rejection correctly.
Fiber v3 implements the sliding window algorithm — fairer than a fixed window:
app.Use(limiter.New(limiter.Config{
Max: 100,
Expiration: time.Minute,
}))With a fixed window, a burst of requests at the end of one minute and the start of the next can combine into 200 within a few seconds. A sliding window counts over a window that keeps shifting — the 100-per-minute limit holds at any point in time. The result is a more precise quota that's hard to exploit with bursts at time boundaries.
By default, limiter state is stored in memory per instance. If the application runs on many servers, each has its own quota. Use Redis so the limiter is consistent globally:
import (
"github.com/gofiber/storage/redis/v3"
"github.com/gofiber/fiber/v3/middleware/limiter"
)
storage := redis.New(redis.Config{
Host: "localhost",
Port: 6379,
Database: 0,
})
app.Use(limiter.New(limiter.Config{
Max: 100,
Expiration: time.Minute,
Storage: storage,
}))limiter.Config{Storage: storage} uses Redis to store request counts. Now a user splitting their quota across two servers is still counted once — the total request count across instances can't break the limit. This is required when the app is deployed behind a load balancer.
for i in $(seq 1 25); do
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/
done | sort | uniq -cThe loop sends 25 requests. With Max: 20, the first 20 return 200 and the rest return 429. The uniq -c count shows the distribution. Try changing the IP in the X-Forwarded-For header and observe that the quota is counted per client.
Key takeaways:
limiter.New(limiter.Config{Max, Expiration}) limits requests per key within a time period.KeyGenerator determines the client identity — read X-Forwarded-For when behind a proxy.LimitReached customizes the response when the limit is hit (status 429).limiter.Config{Storage: redis} shares state across instances for multi-server apps.In the next episode, episode 21, we discuss deployment and Docker — building a Fiber image with standalone output, multi-stage optimization, environment variables, and deploying to Vercel and Docker.