go
72 lines · 7 steps
Per-IP rate limiting middleware in Gin
A Gin middleware that gives each client IP its own token-bucket limiter and evicts stale ones in the background.
Explained by
highlit
1package middleware
2
3import (
4 "net/http"
5 "sync"
6 "time"
7
8 "github.com/gin-gonic/gin"
9 "golang.org/x/time/rate"
10)
11
12type ipLimiter struct {
13 limiter *rate.Limiter
14 lastSeen time.Time
15}
16
17type rateLimiterStore struct {
18 mu sync.Mutex
19 clients map[string]*ipLimiter
20 r rate.Limit
21 burst int
22}
23
24func newStore(r rate.Limit, burst int) *rateLimiterStore {
25 s := &rateLimiterStore{
26 clients: make(map[string]*ipLimiter),
27 r: r,
28 burst: burst,
29 }
30 go s.cleanup()
31 return s
32}
33
34func (s *rateLimiterStore) get(ip string) *rate.Limiter {
35 s.mu.Lock()
36 defer s.mu.Unlock()
37
38 c, ok := s.clients[ip]
39 if !ok {
40 c = &ipLimiter{limiter: rate.NewLimiter(s.r, s.burst)}
41 s.clients[ip] = c
42 }
43 c.lastSeen = time.Now()
44 return c.limiter
45}
46
47func (s *rateLimiterStore) cleanup() {
48 for range time.Tick(time.Minute) {
49 s.mu.Lock()
50 for ip, c := range s.clients {
51 if time.Since(c.lastSeen) > 3*time.Minute {
52 delete(s.clients, ip)
53 }
54 }
55 s.mu.Unlock()
56 }
57}
58
59func PerIPRateLimit(rps float64, burst int) gin.HandlerFunc {
60 store := newStore(rate.Limit(rps), burst)
61
62 return func(c *gin.Context) {
63 if !store.get(c.ClientIP()).Allow() {
64 c.Header("Retry-After", "1")
65 c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
66 "error": "rate limit exceeded",
67 })
68 return
69 }
70 c.Next()
71 }
72}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A map of per-key limiters lets you enforce fairness for each client instead of one global bucket.
- 2Any long-lived map keyed by unbounded input needs a cleanup routine to avoid a slow memory leak.
- 3A single mutex around shared map access keeps concurrent request handlers from racing.
Related explainers
go
package streaming import ( "bufio"
Streaming NDJSON logs over HTTP in Go
http-streaming
channels
select
Advanced
10 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
go
package api import ( "crypto/sha256"
ETag conditional requests in Gin
http-caching
etag
conditional-requests
Intermediate
6 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
go
package logging import ( "context"
Deduplicating log attributes in Go's slog
decorator-pattern
structured-logging
immutability
Intermediate
8 steps
python
import secrets from django.contrib.auth import authenticate, login from django.core.cache import cache
Two-factor login with OTP in Django
two-factor-auth
one-time-passwords
caching
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/per-ip-rate-limiting-middleware-in-gin-explained-go-4d2d/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.