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 render import ( "net/http"
How Gin renders MsgPack responses
serialization
content-type
interface-implementation
Intermediate
5 steps
rust
use axum::{ body::{Body, Bytes}, extract::Request, http::StatusCode,
Logging request bodies in Axum middleware
middleware
streaming-body
logging
Intermediate
8 steps
go
package proxy import ( "log"
Building a hardened Go reverse proxy
reverse-proxy
http
timeouts
Intermediate
7 steps
typescript
import { AsyncLocalStorage } from 'node:async_hooks'; import { randomUUID } from 'node:crypto'; import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express';
Request correlation IDs in NestJS with AsyncLocalStorage
async-local-storage
middleware
logging
Advanced
8 steps
rust
use std::time::Duration; use axum::{ body::Body,
Turning Axum timeouts into 504 JSON
middleware
timeouts
error-handling
Intermediate
7 steps
go
package tsvio import ( "bufio"
Reading and writing TSV records in Go
parsing
io-streams
error-handling
Intermediate
10 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.