diff --git a/rate-limit-redis/README.md b/rate-limit-redis/README.md new file mode 100644 index 0000000000..00f1de1199 --- /dev/null +++ b/rate-limit-redis/README.md @@ -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. diff --git a/rate-limit-redis/config/redis.go b/rate-limit-redis/config/redis.go new file mode 100644 index 0000000000..7ef30502c7 --- /dev/null +++ b/rate-limit-redis/config/redis.go @@ -0,0 +1,20 @@ +package config + +import ( + "fmt" + + "github.com/redis/go-redis/v9" +) + +var RedisClient *redis.Client + +func RedisInit() { + RedisClient = redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", + DB: 0, + Protocol: 2, + }) + + fmt.Println("Redis initialized") +} diff --git a/rate-limit-redis/go.mod b/rate-limit-redis/go.mod new file mode 100644 index 0000000000..08ad99f609 --- /dev/null +++ b/rate-limit-redis/go.mod @@ -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 +) diff --git a/rate-limit-redis/go.sum b/rate-limit-redis/go.sum new file mode 100644 index 0000000000..5a7ec4a754 --- /dev/null +++ b/rate-limit-redis/go.sum @@ -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= diff --git a/rate-limit-redis/handlers/home.go b/rate-limit-redis/handlers/home.go new file mode 100644 index 0000000000..ea1570d9a5 --- /dev/null +++ b/rate-limit-redis/handlers/home.go @@ -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") +} diff --git a/rate-limit-redis/main.go b/rate-limit-redis/main.go new file mode 100644 index 0000000000..80d0919196 --- /dev/null +++ b/rate-limit-redis/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "ratelimitfiber/config" + "ratelimitfiber/middleware" + "time" + + "ratelimitfiber/handlers" + + "github.com/gofiber/fiber/v2" +) + +func main() { + + app := fiber.New() + config.RedisInit() + app.Use(middleware.RateLimiterMiddleware(config.RedisClient, time.Minute, 10)) + + app.Get("/home", handlers.Home) + + app.Listen(":8080") +} diff --git a/rate-limit-redis/middleware/limiter.go b/rate-limit-redis/middleware/limiter.go new file mode 100644 index 0000000000..b2199ab1f0 --- /dev/null +++ b/rate-limit-redis/middleware/limiter.go @@ -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 { + return func(c *fiber.Ctx) error { + ctx := context.Background() + key := fmt.Sprintf("rate-limit:%s", c.IP()) + + 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) + } + if count > int64(maxRequests) { + ttlVal, _ := client.TTL(ctx, key).Result() + c.Set("Retry-After", fmt.Sprintf("%.0f", ttlVal.Seconds())) + return c.Status(429).JSON(fiber.Map{ + "error": "Too Many Requests", + }) + } + return c.Next() + } +}