go
54 lines · 8 steps
Per-tenant daily rate limiting in Gin
A Gin middleware that enforces a daily request quota per tenant using an atomic Redis counter.
Explained by
highlit
1package middleware
2
3import (
4 "context"
5 "net/http"
6 "time"
7
8 "github.com/gin-gonic/gin"
9 "github.com/redis/go-redis/v9"
10)
11
12func EnforceQuota(rdb *redis.Client, dailyLimit int64) gin.HandlerFunc {
13 return func(c *gin.Context) {
14 tenantID := c.GetString("tenant_id")
15 if tenantID == "" {
16 c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
17 "error": "missing tenant context",
18 })
19 return
20 }
21
22 ctx, cancel := context.WithTimeout(c.Request.Context(), 200*time.Millisecond)
23 defer cancel()
24
25 key := "quota:" + tenantID + ":" + time.Now().UTC().Format("2006-01-02")
26
27 remaining, err := rdb.DecrBy(ctx, key, 1).Result()
28 if err != nil {
29 c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{
30 "error": "quota service unavailable",
31 })
32 return
33 }
34
35 if remaining == dailyLimit-1 {
36 rdb.Expire(ctx, key, 24*time.Hour)
37 }
38
39 if remaining < 0 {
40 rdb.Incr(ctx, key)
41 c.Header("X-RateLimit-Remaining", "0")
42 c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
43 "error": "daily quota exceeded",
44 "limit": dailyLimit,
45 "retry_after": "24h",
46 })
47 return
48 }
49
50 c.Header("X-RateLimit-Limit", strconv.FormatInt(dailyLimit, 10))
51 c.Header("X-RateLimit-Remaining", strconv.FormatInt(remaining, 10))
52 c.Next()
53 }
54}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1An atomic decrement lets you check and consume quota in a single race-free operation.
- 2Setting the counter's expiry only on its first decrement gives you a naturally resetting daily window.
- 3Bounding the Redis call with a context timeout keeps a slow dependency from stalling every request.
Related explainers
go
package metrics import ( "sync"
A thread-safe sliding-window average in Go
concurrency
sliding-window
running-average
Intermediate
8 steps
javascript
const { Pool } = require('pg'); const pool = new Pool({ connectionString: process.env.DATABASE_URL,
Per-request Postgres connections in Express
connection-pooling
middleware
transactions
Intermediate
8 steps
go
package logging import ( "fmt"
Redacting secrets with Go reflection
reflection
recursion
struct-tags
Advanced
10 steps
java
public class RequestThrottler { private final Semaphore permits; private final long acquireTimeoutMillis;
Bounding concurrency with a Semaphore in Java
concurrency
semaphore
rate-limiting
Intermediate
6 steps
python
import time import threading from flask import Flask, request, jsonify, g
A token-bucket rate limiter in Flask
rate-limiting
token-bucket
middleware
Intermediate
7 steps
rust
use axum::{ body::{Body, Bytes}, extract::Request, http::StatusCode,
Logging request and response sizes in Axum
middleware
http
streaming-bodies
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-tenant-daily-rate-limiting-in-gin-explained-go-b70e/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.