-
-
Notifications
You must be signed in to change notification settings - Fork 490
feat: add rate limiter middleware recipe using Redis #3199
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# Redis Rate Limiter Middleware for Fiber | ||
|
||
This recipe demonstrates how to build a custom rate limiter middleware in [Fiber](https://github.com/gofiber/fiber) using [Redis](https://redis.io/) as the backend. | ||
|
||
The middleware limits incoming requests per IP address within a given time window (e.g., 10 requests per minute). | ||
|
||
## 📦 Stack | ||
|
||
- Go | ||
- Fiber Web Framework | ||
- Redis (go-redis v9) | ||
|
||
## 📁 Project Structure | ||
|
||
rate-limit-redis/ | ||
├── config/ | ||
│ └── redis.go # Initializes Redis client | ||
├── handlers/ | ||
│ └── home.go # Example handler | ||
├── middleware/ | ||
│ └── limiter.go # Rate limiting logic | ||
├── main.go # Entry point | ||
|
||
|
||
## 🚀 Getting Started | ||
|
||
### 1. Run Redis locally | ||
Make sure Redis is running on `localhost:6379`. You can use Docker: | ||
|
||
```bash | ||
docker run -p 6379:6379 redis | ||
``` | ||
### 2. Run the project | ||
go run main.go | ||
|
||
### 3. Test it | ||
You can hit the endpoint repeatedly: | ||
|
||
```bash | ||
curl http://localhost:8080/home | ||
``` | ||
|
||
After 10 requests (within 60 seconds), you’ll receive: | ||
|
||
```json | ||
{ | ||
"error": "Too Many Requests" | ||
} | ||
|
||
``` | ||
|
||
⚙️ Configuration | ||
You can modify rate limit logic by changing: | ||
app.Use(RateLimiterMiddleware(redisClient, time.Minute, 10)) | ||
|
||
|
||
✨ Features | ||
Rate limiting per IP | ||
|
||
Redis-backed counter for persistence and performance | ||
|
||
Retry-After header for graceful client handling | ||
|
||
🙌 Contributing | ||
Feel free to open issues or PRs if you'd like to improve this recipe! | ||
|
||
|
||
🧠 Inspired by production needs where APIs require protection against brute-force abuse and overuse. |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,20 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
package config | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
"fmt" | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
"github.com/redis/go-redis/v9" | ||||||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
var RedisClient *redis.Client | ||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
func RedisInit() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
RedisClient = redis.NewClient(&redis.Options{ | ||||||||||||||||||||||||||||||||||||||||||||||||||
Addr: "localhost:6379", | ||||||||||||||||||||||||||||||||||||||||||||||||||
Password: "", | ||||||||||||||||||||||||||||||||||||||||||||||||||
DB: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||
Protocol: 2, | ||||||||||||||||||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
fmt.Println("Redis initialized") | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+11
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Return a connection error instead of assuming success
-func RedisInit() {
- RedisClient = redis.NewClient(&redis.Options{
+func RedisInit() error {
+ RedisClient = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
Protocol: 2,
})
-
- fmt.Println("Redis initialized")
+ if err := RedisClient.Ping(context.Background()).Err(); err != nil {
+ return fmt.Errorf("redis ping failed: %w", err)
+ }
+ fmt.Println("Redis initialized")
+ return nil
} Call-site changes required in 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
module ratelimitfiber | ||
|
||
go 1.23.1 | ||
|
||
require ( | ||
github.com/gofiber/fiber/v2 v2.52.8 | ||
github.com/redis/go-redis/v9 v9.10.0 | ||
) | ||
|
||
require ( | ||
github.com/andybalholm/brotli v1.1.0 // indirect | ||
github.com/cespare/xxhash/v2 v2.3.0 // indirect | ||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect | ||
github.com/google/uuid v1.6.0 // indirect | ||
github.com/klauspost/compress v1.17.9 // indirect | ||
github.com/mattn/go-colorable v0.1.13 // indirect | ||
github.com/mattn/go-isatty v0.0.20 // indirect | ||
github.com/mattn/go-runewidth v0.0.16 // indirect | ||
github.com/rivo/uniseg v0.2.0 // indirect | ||
github.com/valyala/bytebufferpool v1.0.0 // indirect | ||
github.com/valyala/fasthttp v1.51.0 // indirect | ||
github.com/valyala/tcplisten v1.0.0 // indirect | ||
golang.org/x/sys v0.28.0 // indirect | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= | ||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= | ||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= | ||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= | ||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= | ||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= | ||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= | ||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | ||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= | ||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= | ||
github.com/gofiber/fiber/v2 v2.52.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0x4= | ||
github.com/gofiber/fiber/v2 v2.52.8/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= | ||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= | ||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= | ||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= | ||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= | ||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= | ||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= | ||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= | ||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= | ||
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs= | ||
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= | ||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= | ||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | ||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= | ||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= | ||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= | ||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= | ||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= | ||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= | ||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= | ||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package handlers | ||
|
||
import ( | ||
"github.com/gofiber/fiber/v2" | ||
) | ||
|
||
func Home(c *fiber.Ctx) error { | ||
return c.Status(fiber.StatusOK).SendString("This is home api") | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,22 @@ | ||||||||||||||
package main | ||||||||||||||
|
||||||||||||||
import ( | ||||||||||||||
"ratelimitfiber/config" | ||||||||||||||
"ratelimitfiber/middleware" | ||||||||||||||
"time" | ||||||||||||||
|
||||||||||||||
"ratelimitfiber/handlers" | ||||||||||||||
|
||||||||||||||
"github.com/gofiber/fiber/v2" | ||||||||||||||
) | ||||||||||||||
Comment on lines
+3
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Module import path looks wrong The code imports 🤖 Prompt for AI Agents
|
||||||||||||||
|
||||||||||||||
func main() { | ||||||||||||||
|
||||||||||||||
app := fiber.New() | ||||||||||||||
config.RedisInit() | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||
app.Use(middleware.RateLimiterMiddleware(config.RedisClient, time.Minute, 10)) | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line uses the global
Suggested change
|
||||||||||||||
|
||||||||||||||
Comment on lines
+16
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Handle Redis init failures After refactoring - config.RedisInit()
+ if err := config.RedisInit(); err != nil {
+ log.Fatalf("unable to start: %v", err)
+ } 🤖 Prompt for AI Agents
|
||||||||||||||
app.Get("/home", handlers.Home) | ||||||||||||||
|
||||||||||||||
app.Listen(":8080") | ||||||||||||||
} | ||||||||||||||
Comment on lines
+21
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Check listen error
- app.Listen(":8080")
+ if err := app.Listen(":8080"); err != nil {
+ log.Fatalf("fiber failed: %v", err)
+ } 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,36 @@ | ||||||||||||||||||||||||||||||||||
package middleware | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
import ( | ||||||||||||||||||||||||||||||||||
"context" | ||||||||||||||||||||||||||||||||||
"fmt" | ||||||||||||||||||||||||||||||||||
"time" | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
"github.com/gofiber/fiber/v2" | ||||||||||||||||||||||||||||||||||
"github.com/redis/go-redis/v9" | ||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
func RateLimiterMiddleware(client *redis.Client, ttl time.Duration, maxRequests int) fiber.Handler { | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not use the limiter middleware with the storage adapater for redis ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I intentionally built a custom Redis-based middleware to demonstrate how Fiber middleware can be extended or customized for specific use cases. That said, I will update the README that Fiber already provides a built-in rate limiting middleware with Redis support, which is ideal for most production scenarios. |
||||||||||||||||||||||||||||||||||
return func(c *fiber.Ctx) error { | ||||||||||||||||||||||||||||||||||
ctx := context.Background() | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using
Suggested change
|
||||||||||||||||||||||||||||||||||
key := fmt.Sprintf("rate-limit:%s", c.IP()) | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
count, err := client.Incr(ctx, key).Result() | ||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||
return c.Status(500).JSON(fiber.Map{ | ||||||||||||||||||||||||||||||||||
"error": "Redis Error", | ||||||||||||||||||||||||||||||||||
"details": err.Error(), | ||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
if count == 1 { | ||||||||||||||||||||||||||||||||||
client.Expire(ctx, key, ttl) | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
Comment on lines
+24
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Ignore-error write to Redis If - if count == 1 {
- client.Expire(ctx, key, ttl)
+ if count == 1 {
+ if err := client.Expire(ctx, key, ttl).Err(); err != nil {
+ return c.Status(500).JSON(fiber.Map{
+ "error": "Redis Error",
+ "details": err.Error(),
+ })
+ }
} 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||
if count > int64(maxRequests) { | ||||||||||||||||||||||||||||||||||
ttlVal, _ := client.TTL(ctx, key).Result() | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error returned by ttlVal, err := client.TTL(ctx, key).Result()
if err != nil && err != redis.Nil {
// Handle TTL error, maybe log it or return a 500
// For this recipe, we might just log and continue with 0 retry-after
fmt.Printf("Error getting TTL for key %s: %v\n", key, err)
ttlVal = 0 // Default to 0 if TTL retrieval fails
} |
||||||||||||||||||||||||||||||||||
c.Set("Retry-After", fmt.Sprintf("%.0f", ttlVal.Seconds())) | ||||||||||||||||||||||||||||||||||
return c.Status(429).JSON(fiber.Map{ | ||||||||||||||||||||||||||||||||||
"error": "Too Many Requests", | ||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
Comment on lines
+27
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion TTL can be −1; guard before using
- ttlVal, _ := client.TTL(ctx, key).Result()
- c.Set("Retry-After", fmt.Sprintf("%.0f", ttlVal.Seconds()))
+ ttlVal, err := client.TTL(ctx, key).Result()
+ if err == nil && ttlVal > 0 {
+ c.Set("Retry-After", fmt.Sprintf("%.0f", ttlVal.Seconds()))
+ } 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||
return c.Next() | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pls follow the https://github.com/gofiber/recipes/blob/master/.github/CONTRIBUTING.md steps