go
69 lines · 8 steps
A per-IP rate limiter middleware in Gin
A token-bucket limiter tracked per client IP, wired into Gin as middleware that rejects requests over the allowed rate.
Explained by
highlit
1package middleware
2
3import (
4 "net/http"
5 "strconv"
6 "sync"
7 "time"
8
9 "github.com/gin-gonic/gin"
10 "golang.org/x/time/rate"
11)
12
13type visitor struct {
14 limiter *rate.Limiter
15 lastSeen time.Time
16}
17
18type RateLimiter struct {
19 mu sync.Mutex
20 visitors map[string]*visitor
21 rps rate.Limit
22 burst int
23}
24
25func NewRateLimiter(rps float64, burst int) *RateLimiter {
26 rl := &RateLimiter{
27 visitors: make(map[string]*visitor),
28 rps: rate.Limit(rps),
29 burst: burst,
30 }
31 return rl
32}
33
34func (rl *RateLimiter) limiterFor(key string) *rate.Limiter {
35 rl.mu.Lock()
36 defer rl.mu.Unlock()
37
38 v, ok := rl.visitors[key]
39 if !ok {
40 v = &visitor{limiter: rate.NewLimiter(rl.rps, rl.burst)}
41 rl.visitors[key] = v
42 }
43 v.lastSeen = time.Now()
44 return v.limiter
45}
46
47func (rl *RateLimiter) Middleware() gin.HandlerFunc {
48 return func(c *gin.Context) {
49 limiter := rl.limiterFor(c.ClientIP())
50 reservation := limiter.Reserve()
51
52 if !reservation.OK() {
53 c.AbortWithStatus(http.StatusInternalServerError)
54 return
55 }
56
57 if delay := reservation.Delay(); delay > 0 {
58 reservation.Cancel()
59 retryAfter := int(delay.Seconds() + 0.999)
60 c.Header("Retry-After", strconv.Itoa(retryAfter))
61 c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
62 "error": "rate limit exceeded",
63 })
64 return
65 }
66
67 c.Next()
68 }
69}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A token-bucket limiter smooths request rates while allowing short bursts up to a configured ceiling.
- 2Keeping a map of limiters keyed by client identity gives each visitor an independent quota.
- 3Reserving instead of blocking lets the middleware reject over-limit requests immediately with a Retry-After hint.
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/a-per-ip-rate-limiter-middleware-in-gin-explained-go-b0b9/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.