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
java
public class SlidingLogRateLimiter { private final int maxRequests; private final long windowMillis;
How a sliding-log rate limiter works
rate-limiting
concurrency
sliding-window
Advanced
8 steps
go
package theme import ( "fmt"
Per-tenant HTML templates with Gin's renderer
multi-tenancy
concurrency
html-templates
Advanced
8 steps
python
import time import threading from enum import Enum from functools import wraps
Building a circuit breaker in Python
circuit-breaker
resilience
decorators
Advanced
7 steps
typescript
import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'; import { BehaviorSubject, Observable, throwError } from 'rxjs'; import { catchError, filter, take, switchMap } from 'rxjs/operators';
Refreshing auth tokens in an Angular interceptor
http-interceptor
token-refresh
rxjs
Advanced
8 steps
go
package auth import ( "net/http"
Setting and reading secure session cookies in Go
cookies
session-management
security
Intermediate
6 steps
php
<?php namespace App\Http\Middleware;
Idempotent requests with Laravel middleware
idempotency
middleware
caching
Advanced
8 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.