๐Ÿš€ Build a Redis-Powered API with Atomic Operations, Rate Limiting, and Real-Time Metrics โ€” Complete Docker Tutorial


Excellent โ€” hereโ€™s your definitive, single-source Redis + FastAPI + Prometheus + Grafana guide, ready to publish as a full blog post, e-book chapter, or narrated video script. Everything is consolidated: code, folder tree, line explanations, tests, metrics, Grafana use, and storytelling intro/outro.


๐Ÿš€ Redis, FastAPI, Prometheus & Grafana โ€” A Complete, Containerized Guide

Build, Observe, and Master Atomic Operations, Rate Limiting, and Real-Time Dashboards


๐ŸŽฌ Intro โ€“ Why This Matters

Modern APIs are not just about serving requests; theyโ€™re about control โ€” controlling load, speed, and visibility. A single user or bot can flood your endpoints, making your service unreliable. Thatโ€™s where Redis steps in โ€” a lightning-fast in-memory data store, capable of atomic increments, token-bucket algorithms, and millions of ops per second.

But control without visibility is blind. To see whatโ€™s really happening, you need Prometheus and Grafana โ€” tools that turn raw metrics into real-time dashboards.

This tutorial builds a complete system, from Redis operations to Grafana dashboards, inside a single Docker Compose setup.


๐Ÿงฑ Project Overview

redis-redislim/
โ”œโ”€ docker-compose.yml
โ”œโ”€ .env
โ”œโ”€ api/
โ”‚  โ”œโ”€ Dockerfile
โ”‚  โ”œโ”€ requirements.txt
โ”‚  โ””โ”€ app/
โ”‚     โ”œโ”€ __init__.py
โ”‚     โ”œโ”€ redis_client.py
โ”‚     โ”œโ”€ ratelimit.py
โ”‚     โ”œโ”€ metrics.py
โ”‚     โ”œโ”€ main.py
โ”‚     โ””โ”€ scripts/load_test.py
โ”œโ”€ prometheus/prometheus.yml
โ””โ”€ grafana/
   โ”œโ”€ provisioning/
   โ”‚  โ”œโ”€ datasources/datasource.yml
   โ”‚  โ””โ”€ dashboards/dashboards.yml
   โ””โ”€ dashboards/redis_rate_limiter.json

Youโ€™ll learn:


๐Ÿงฐ Tech Stack

Component Purpose Port
Redis Atomic counters, Lua logic 6379
Redis Commander GUI key explorer 8081
FastAPI Core REST API 8001
Prometheus Metrics collector 9090
Grafana Dashboard visualizer 3000

๐Ÿงฉ docker-compose.yml (line-by-line)

version: "3.9"

Compose spec version.

services:

Defines all services.

redis

  redis:
    image: redis:7-alpine
    command: ["redis-server", "--appendonly", "yes", "--save", "60", "1000"]
    ports:
      - "6379:6379"
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "PING"]
      interval: 5s
      timeout: 3s
      retries: 20

redis-commander

  redis-commander:
    image: rediscommander/redis-commander:latest
    environment:
      - REDIS_HOSTS=local:redis:6379
    ports:
      - "8081:8081"
    depends_on:
      - redis

Browser GUI for viewing Redis keys.

api

  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    env_file:
      - .env
    depends_on:
      redis:
        condition: service_healthy
    ports:
      - "8001:8001"
    volumes:
      - ./api/app:/app/app

prometheus

  prometheus:
    image: prom/prometheus:latest
    command: ["--config.file=/etc/prometheus/prometheus.yml", "--storage.tsdb.retention.time=2d"]
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
    ports:
      - "9090:9090"
    depends_on:
      - api

Prometheus scrapes metrics from the API.

grafana

  grafana:
    image: grafana/grafana:latest
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_USERS_DEFAULT_THEME=dark
    ports:
      - "3000:3000"
    volumes:
      - ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro
      - ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro
      - ./grafana/dashboards:/var/lib/grafana/dashboards:ro
    depends_on:
      - prometheus

Grafana auto-provisions Prometheus datasource and loads dashboards.

volumes:
  redisdata:

Persistent volume for Redis.


โš™๏ธ .env

REDIS_URL=redis://redis:6379/0
APP_HOST=0.0.0.0
APP_PORT=8001
FIXED_WINDOW_LIMIT=5
FIXED_WINDOW_SECONDS=10
SLIDING_WINDOW_LIMIT=8
SLIDING_WINDOW_SECONDS=10

Stores configuration and limits.


๐Ÿ API Code Explained

๐Ÿ“ฆ redis_client.py

Creates a reusable Redis connection.

from redis.asyncio import Redis
import os

_redis: Redis | None = None

async def get_redis() -> Redis:
    global _redis
    if _redis is None:
        url = os.getenv("REDIS_URL", "redis://localhost:6379/0")
        _redis = Redis.from_url(url, encoding="utf-8", decode_responses=True)
    return _redis

โšก ratelimit.py

Implements Fixed Window, Sliding Window, Token Bucket.

1๏ธโƒฃ Fixed Window

async def fixed_window_allow(redis, key, limit, window_secs):
    rk = f"rl:fixed:{key}"
    async with redis.pipeline(transaction=True) as pipe:
        while True:
            try:
                await pipe.watch(rk)
                exists = await redis.exists(rk)
                pipe.multi()
                pipe.incr(rk, 1)
                if not exists:
                    pipe.expire(rk, window_secs)
                res = await pipe.execute()
                new_count = res[0]
                ttl = await redis.ttl(rk)
                allowed = new_count <= limit
                remaining = max(limit - new_count, 0)
                return allowed, remaining, ttl
            except Exception:
                continue
            finally:
                await pipe.reset()

Uses atomic INCR and EXPIRE for simple burst control.

2๏ธโƒฃ Sliding Window

async def sliding_window_allow(redis, key, limit, window_secs):
    rk = f"rl:slide:{key}"
    now_ms = int(time.time() * 1000)
    window_start = now_ms - window_secs * 1000
    async with redis.pipeline(transaction=True) as pipe:
        pipe.zremrangebyscore(rk, 0, window_start)
        pipe.zadd(rk, {str(now_ms): now_ms})
        pipe.zcard(rk)
        pipe.expire(rk, window_secs * 2)
        _, _, count, _ = await pipe.execute()
    return count <= limit, max(limit - count, 0)

Counts timestamps within the last N seconds using a sorted set.

3๏ธโƒฃ Token Bucket

TOKEN_BUCKET_LUA = """
local tokens_key = KEYS[1]
local ts_key = KEYS[2]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now_ms = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
local tokens = tonumber(redis.call('GET', tokens_key))
local last_ts = tonumber(redis.call('GET', ts_key))
if tokens == nil then tokens = capacity end
if last_ts == nil then last_ts = now_ms end
local delta = math.max(0, now_ms - last_ts)
local refill = (delta / 1000.0) * refill_rate
tokens = math.min(capacity, tokens + refill)
local allowed = 0
if tokens >= cost then tokens = tokens - cost; allowed = 1 end
redis.call('SET', tokens_key, tokens)
redis.call('SET', ts_key, now_ms)
redis.call('PEXPIRE', tokens_key, 7200000)
redis.call('PEXPIRE', ts_key, 7200000)
return {allowed, tokens}
"""

Runs atomically on the Redis server โ€” no race conditions.


๐Ÿ“ˆ metrics.py

Defines Prometheus metrics and /metrics endpoint.

from prometheus_client import Counter, Histogram, CONTENT_TYPE_LATEST, generate_latest
from starlette.responses import Response

REQUESTS_TOTAL = Counter("requests_total", "Total API requests", ["endpoint","status","strategy"])
RATE_LIMITED_TOTAL = Counter("rate_limited_total", "Rate-limited responses", ["strategy"])
REQUEST_LATENCY = Histogram("request_latency_seconds", "Latency of requests in seconds")

async def metrics_endpoint():
    return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)

๐Ÿงฎ main.py

Ties it all together.

from fastapi import FastAPI, Depends, Request, Query
from redis.asyncio import Redis
from .redis_client import get_redis
from .ratelimit import fixed_window_allow, sliding_window_allow, token_bucket_allow
from .metrics import REQUESTS_TOTAL, RATE_LIMITED_TOTAL, REQUEST_LATENCY, metrics_endpoint
import time, os

Create app + middleware

app = FastAPI(title="Redis A-to-Z: Atomic Ops & Rate Limiting + Metrics")

@app.middleware("http")
async def track_latency(request: Request, call_next):
    start = time.perf_counter()
    response = await call_next(request)
    REQUEST_LATENCY.observe(time.perf_counter() - start)
    return response

Health check

@app.get("/health")
async def health(redis: Redis = Depends(get_redis)):
    pong = await redis.ping()
    return {"ok": pong}

Rate limiter endpoints

(Fixed, Sliding, Token Bucket) โ€” call the functions above, record metrics, and return JSON results.


๐Ÿงช load_test.py

Concurrent tester using aiohttp to generate load.


๐Ÿ“Š Prometheus Setup

prometheus.yml:

global:
  scrape_interval: 5s
scrape_configs:
  - job_name: "api"
    static_configs:
      - targets: ["api:8001"]
    metrics_path: /metrics

Tells Prometheus to poll the API every 5 s for metrics.

Browse: http://localhost:9090

Try:

requests_total
rate(requests_total[1m])
histogram_quantile(0.9, sum(rate(request_latency_seconds_bucket[1m])) by (le))

๐ŸŽจ Grafana Setup

Pre-configured datasource (grafana/provisioning/datasources/datasource.yml) and dashboard definition (grafana/dashboards/redis_rate_limiter.json).

Run โ†’ http://localhost:3000 โ†’ login admin/admin.

Dashboard Panels:


๐Ÿงช Testing the API (and why it works)

Test Command Why it works
Health curl http://localhost:8001/health Confirms Redis โ†” API link
Atomic Counter curl http://localhost:8001/counter/incr?key=page Redis INCR ensures atomicity
Stock Transfer Transfer items Aโ†’B Uses WATCH/MULTI/EXEC for safe multi-key transaction
Wallet Debit Lua script debit One atomic check-then-update
Rate Limiting Hit /rate/fixed repeatedly INCR + TTL window
Sliding Window Hit /rate/sliding ZSET timestamps for smoother throttling
Token Bucket Hit /rate/tokenbucket Lua atomic refill + consume logic
Metrics curl [http://localhost:8001/metrics](http://localhost:8001/metrics) | head Prometheus-compatible export
Grafana View localhost:3000 Live visualization

๐Ÿง  Understanding Prometheus Flow

  1. FastAPI exposes /metrics.
  2. Prometheus scrapes it.
  3. Each scrape stores a timestamp/value pair.
  4. Grafana queries Prometheus with PromQL expressions like:

    sum(rate(requests_total[1m])) by (endpoint)
  5. Charts update every few seconds.

Prometheus = collector Grafana = visualizer


๐ŸŽฅ Suggested Narration (TechieTalksAI Style)

Intro (10 s) โ€œToday weโ€™re building a complete, self-contained monitoring lab with Redis, FastAPI, Prometheus, and Grafana.โ€

Mid (2 min) โ€œEvery call you make to the API instantly updates Redis โ€” counters, windows, and buckets โ€” and Prometheus records those events every few seconds. Grafana visualizes them, turning invisible backend logic into a living dashboard.โ€

Outro (20 s) โ€œIn just minutes, youโ€™ve built a production-grade rate limiter with real-time observability. Redis keeps your system atomic and fast; Prometheus keeps it measurable; Grafana makes it beautiful. Iโ€™m Sreeprakash from TechieTalksAI โ€” see you in the next automation deep dive!โ€


๐Ÿ Summary

Layer Tool Role
Business Logic FastAPI Defines endpoints & metrics
In-Memory Store Redis Atomic operations & rate limits
Metrics Collector Prometheus Records numeric time-series
Visualization Grafana Turns metrics into insight

This consolidated system is your laboratory for API observability โ€” you can test, tune, and visualize rate limiting in real time.


PREVIOUS

๐Ÿงญ Introduction

APIs are the backbone of the internet. Every second, millions of requests hit servers across the world โ€” fetching data, authenticating users, processing payments, or sending messages.

But what happens when too many requests arrive at once? Your server slows down, fails, or โ€” worse โ€” becomes vulnerable to abuse.

Thatโ€™s where Redis shines.

Redis isnโ€™t just a cache โ€” itโ€™s an in-memory atomic database capable of handling millions of operations per second with microsecond latency. Its atomic commands (INCR, ZADD, Lua scripts) make it perfect for:

This project is your complete A-to-Z Redis lab, containerized for instant exploration.

Youโ€™ll learn by building and testing โ€” every concept backed by a live endpoint and a measurable outcome.


๐Ÿงฑ Project Structure

redis-redislim/
โ”œโ”€ docker-compose.yml        # orchestrates everything
โ”œโ”€ .env                      # config for app limits
โ”œโ”€ api/
โ”‚  โ”œโ”€ Dockerfile
โ”‚  โ”œโ”€ requirements.txt
โ”‚  โ””โ”€ app/
โ”‚     โ”œโ”€ main.py             # FastAPI endpoints
โ”‚     โ”œโ”€ redis_client.py     # Redis connection helper
โ”‚     โ”œโ”€ ratelimit.py        # fixed, sliding, token bucket logic
โ”‚     โ”œโ”€ metrics.py          # Prometheus counters & histograms
โ”‚     โ”œโ”€ __init__.py
โ”‚     โ””โ”€ scripts/load_test.py
โ”œโ”€ prometheus/prometheus.yml
โ””โ”€ grafana/
   โ”œโ”€ provisioning/
   โ”‚  โ”œโ”€ datasources/datasource.yml
   โ”‚  โ””โ”€ dashboards/dashboards.yml
   โ””โ”€ dashboards/redis_rate_limiter.json

Each piece is modular and self-contained โ€” no external dependencies other than Docker.


๐Ÿงฐ The Tech Stack

Component Role Port
FastAPI Core API service with Prometheus metrics 8001
Redis Atomic in-memory data store 6379
Redis Commander GUI for live key inspection 8081
Prometheus Metrics collection & scraping 9090
Grafana Visualization dashboards 3000

โš™๏ธ How It Works โ€” A 10,000-Foot View

  1. FastAPI handles HTTP requests for counters, wallets, and rate-limit tests.
  2. Each API call updates Redis atomically using INCR, ZADD, or Lua scripts.
  3. Prometheus scrapes /metrics every 5 seconds to record live data.
  4. Grafana visualizes request rates, limits, and latencies in real-time.
  5. All services run together via docker compose up --build.

๐Ÿงฉ Folder-by-Folder Breakdown

๐Ÿ api/app/redis_client.py

Creates a singleton async Redis connection โ€” safe, fast, reusable.

_redis = Redis.from_url(url, encoding="utf-8", decode_responses=True)

Every FastAPI route can depend on get_redis() for a shared client.

โšก api/app/ratelimit.py

Implements three atomic limiters:

  1. Fixed Window โ€” Uses INCR + EXPIRE. Simple but allows bursts at the window edges.

  2. Sliding Window โ€” Uses ZADD and timestamp trimming. Smooths out bursts by counting recent events only.

  3. Token Bucket โ€” Uses a Lua script for atomic refill and consumption. Perfect for steady, controlled request pacing.

๐Ÿงฎ api/app/metrics.py

Defines Prometheus counters and histograms:

REQUESTS_TOTAL      # per endpoint, status, strategy
RATE_LIMITED_TOTAL  # per strategy
REQUEST_LATENCY     # histogram p50, p90, p99 latency

๐Ÿ—๏ธ api/app/main.py

๐Ÿ“Š prometheus/prometheus.yml

Prometheus scrapes the APIโ€™s /metrics endpoint every 5 seconds.

๐ŸŽจ grafana/ folder

Auto-provisions:


๐Ÿงช Running the Project

From project root:

docker compose up --build

After 30 seconds, visit:

Service URL Description
FastAPI http://localhost:8001/docs Interactive Swagger UI
Redis Commander http://localhost:8081 See Redis keys live
Prometheus http://localhost:9090 Raw metrics explorer
Grafana http://localhost:3000 admin / admin
Redis localhost:6379 optional CLI access

โœ… Step-by-Step Testing & Explanations


1. Health Check

curl -s http://localhost:8001/health
# โ†’ {"ok": true}

Why it works: get_redis() pings Redis. If Redis is reachable, FastAPI returns ok: true.


2. Atomic Counter Test

curl -s "http://localhost:8001/counter/incr?key=page_views"
curl -s "http://localhost:8001/counter/get?key=page_views"

What happens:

Why it works: Redis INCR is single-threaded and atomic โ€” even under heavy concurrency.


3. Transaction Test (WATCH/MULTI/EXEC)

curl -s "http://localhost:8001/stock/seed?item=a&qty=50" -X POST
curl -s "http://localhost:8001/stock/seed?item=b&qty=10" -X POST
curl -s "http://localhost:8001/stock/transfer" \
  -H "Content-Type: application/json" \
  -d '{"from_item":"a","to_item":"b","qty":7}' -X POST

What happens:

Why it works: Redis transactions guarantee atomic updates when executed within MULTI/EXEC.


4. Lua Wallet Test (Atomic Debit)

curl -s "http://localhost:8001/wallet/seed?user=sree&amount=100" -X POST
curl -s "http://localhost:8001/wallet/debit?user=sree&amount=30" -X POST
curl -s "http://localhost:8001/wallet/debit?user=sree&amount=100" -X POST

Expected:

Why it works: Lua scripts in Redis run atomically in one thread. The debit logic:

if balance >= amount then
  redis.call('DECRBY', key, amount)

guarantees no inconsistent balance.


5. Rate Limiting Tests

Each limiter uses a composite key: <X-API-Key>:<client-ip> โ€” ensuring fair separation of users and IPs.

a) Fixed Window

for i in {1..7}; do
  curl -s -H "X-API-Key: demo123" "http://localhost:8001/rate/fixed";
done

Expected: First 5 allowed, next 2 blocked with allowed: false, retry_in: X.

Why: INCR counts per 10-second window. Once limit = 5, remaining resets after expiry.


b) Sliding Window

for i in {1..12}; do
  curl -s -H "X-API-Key: demo123" "http://localhost:8001/rate/sliding";
done

Expected: Fewer blocks โ€” because requests โ€œslideโ€ off after 10s.

Why: Uses ZADD timestamps and ZREMRANGEBYSCORE to count active events within the window.


c) Token Bucket

for i in {1..15}; do
  curl -s -H "X-API-Key: gold-user" \
  "http://localhost:8001/rate/tokenbucket?capacity=10&refill_per_sec=2&cost=1";
done
sleep 3
for i in {1..5}; do
  curl -s -H "X-API-Key: gold-user" \
  "http://localhost:8001/rate/tokenbucket?capacity=10&refill_per_sec=2&cost=1";
done

Expected:

Why it works: The Lua script atomically updates both token count and timestamp. No race condition โ€” perfect for multi-instance deployments.


6. Observe Keys in Redis Commander

Visit http://localhost:8081 and watch keys appear:

Strategy Key Pattern Type
Fixed rl:fixed:<key> string
Sliding rl:slide:<key> zset
Token Bucket rl:tb:tokens:<key> & rl:tb:ts:<key> string

TTL counts down automatically (10s for fixed/sliding, 2h for token bucket).


7. Prometheus Metrics

Check raw metrics:

curl -s http://localhost:8001/metrics | head -n 20

Look for:

requests_total{endpoint="/rate/fixed",status="200",strategy="fixed"} 5.0
rate_limited_total{strategy="tokenbucket"} 2.0
request_latency_seconds_bucket{le="0.5"} 7.0

Why it works: Every request increments counters; FastAPI middleware measures latency and exports via /metrics.


8. Grafana Visualization

Open Grafana โ†’ http://localhost:3000 โ†’ login admin/admin. Dashboard: Redis Rate Limiter (API)

Panels:

Why it works: Prometheus scrapes the FastAPI metrics; Grafana queries Prometheus via datasource provisioning.


9. Optional Load Test

python api/app/scripts/load_test.py "http://localhost:8001/rate/fixed" 15
python api/app/scripts/load_test.py "http://localhost:8001/rate/sliding" 20
python api/app/scripts/load_test.py "http://localhost:8001/rate/tokenbucket?capacity=10&refill_per_sec=2&cost=1" 20

Why it works: This script sends multiple concurrent requests using aiohttp to test concurrency. Youโ€™ll see OK and BLOCKED logs in terminal, and your Grafana graphs will spike in real-time.


๐Ÿง  Key Takeaways

Concept Redis Feature Outcome
Atomic counter INCR Race-free increments
Sliding window ZADD + score trimming Rolling request count
Token bucket Lua + timestamps Smooth rate control
Transactions WATCH/MULTI/EXEC Safe multi-key updates
Metrics Prometheus client Real-time observability
Visualization Grafana dashboards Live analytics UI

Redis acts as your control layer, FastAPI as your business layer, Prometheus as your data layer, and Grafana as your insight layer โ€” all within Docker.


FILES


you got it โ€” hereโ€™s a clear, file-by-file, line/section-by-line walkthrough of the working project. Iโ€™ll keep the explanations tight but complete so you (or your team) can reason about every decision, change things safely, and debug fast.


docker-compose.yml (orchestration)

version: "3.9"
services:

redis

  redis:
    image: redis:7-alpine
    command: ["redis-server", "--appendonly", "yes", "--save", "60", "1000"]
    ports:
      - "6379:6379"
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "PING"]
      interval: 5s
      timeout: 3s
      retries: 20

redis-commander

  redis-commander:
    image: rediscommander/redis-commander:latest
    environment:
      - REDIS_HOSTS=local:redis:6379
    ports:
      - "8081:8081"
    depends_on:
      - redis

api

  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    env_file:
      - .env
    depends_on:
      redis:
        condition: service_healthy
    ports:
      - "8001:8001"
    volumes:
      - ./api/app:/app/app

prometheus

  prometheus:
    image: prom/prometheus:latest
    command: ["--config.file=/etc/prometheus/prometheus.yml", "--storage.tsdb.retention.time=2d"]
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
    ports:
      - "9090:9090"
    depends_on:
      - api

grafana

  grafana:
    image: grafana/grafana:latest
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_USERS_DEFAULT_THEME=dark
    ports:
      - "3000:3000"
    volumes:
      - ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro
      - ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro
      - ./grafana/dashboards:/var/lib/grafana/dashboards:ro
    depends_on:
      - prometheus
volumes:
  redisdata:

.env (config)

REDIS_URL=redis://redis:6379/0
APP_HOST=0.0.0.0
APP_PORT=8001
FIXED_WINDOW_LIMIT=5
FIXED_WINDOW_SECONDS=10
SLIDING_WINDOW_LIMIT=8
SLIDING_WINDOW_SECONDS=10

api/Dockerfile (API image)

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
EXPOSE 8001
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001", "--reload"]

api/requirements.txt

fastapi==0.115.0        # Web framework
uvicorn==0.30.6         # ASGI server
redis==5.0.7            # Async Redis client
pydantic==2.9.2         # Data validation models
aiohttp==3.10.5         # Load-test helper script
prometheus_client==0.20.0  # /metrics exporter

api/app/init.py

# makes "app" a proper package

api/app/redis_client.py (singleton Redis client)

from redis.asyncio import Redis
import os

_redis: Redis | None = None
async def get_redis() -> Redis:
    global _redis
    if _redis is None:
        url = os.getenv("REDIS_URL", "redis://localhost:6379/0")
        _redis = Redis.from_url(url, encoding="utf-8", decode_responses=True)
    return _redis

Why: connection reuse avoids overhead; async client fits FastAPIโ€™s async routes.


api/app/ratelimit.py (core algorithms)

import time
from typing import Tuple
from redis.asyncio import Redis

Fixed window (INCR + EXPIRE)

async def fixed_window_allow(redis: Redis, key: str, limit: int, window_secs: int) -> Tuple[bool, int, int]:
    rk = f"rl:fixed:{key}"
    async with redis.pipeline(transaction=True) as pipe:
        while True:
            try:
                await pipe.watch(rk)
                exists = await redis.exists(rk)
                pipe.multi()
                pipe.incr(rk, 1)                 # atomic counter++
                if not exists:
                    pipe.expire(rk, window_secs) # start TTL on first hit
                res = await pipe.execute()        # commit tx
                new_count = res[0]
                ttl = await redis.ttl(rk)         # seconds until window resets
                allowed = new_count <= limit
                remaining = max(limit - new_count, 0)
                return allowed, remaining, ttl
            except Exception:
                continue            # on concurrent write conflicts, retry
            finally:
                await pipe.reset()

Sliding window (ZSET of timestamps)

async def sliding_window_allow(redis: Redis, key: str, limit: int, window_secs: int) -> Tuple[bool, int]:
    rk = f"rl:slide:{key}"
    now_ms = int(time.time() * 1000)
    window_start_ms = now_ms - window_secs * 1000

    async with redis.pipeline(transaction=True) as pipe:
        pipe.zremrangebyscore(rk, 0, window_start_ms) # evict old timestamps
        pipe.zadd(rk, {str(now_ms): now_ms})          # add this request
        pipe.zcard(rk)                                 # count active
        pipe.expire(rk, window_secs * 2)               # cleanup if idle later
        _, _, count, _ = await pipe.execute()

    allowed = count <= limit
    remaining = max(limit - count, 0)
    return allowed, remaining

Token bucket (Lua for atomic refill+consume)

TOKEN_BUCKET_LUA = """
local tokens_key = KEYS[1]
local ts_key = KEYS[2]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])  -- tokens/sec
local now_ms = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])

local tokens = tonumber(redis.call('GET', tokens_key))
local last_ts = tonumber(redis.call('GET', ts_key))

if tokens == nil then tokens = capacity end
if last_ts == nil then last_ts = now_ms end

local delta = math.max(0, now_ms - last_ts)
local refill = (delta / 1000.0) * refill_rate
tokens = math.min(capacity, tokens + refill)

local allowed = 0
if tokens >= cost then
  tokens = tokens - cost
  allowed = 1
end

redis.call('SET', tokens_key, tokens)
redis.call('SET', ts_key, now_ms)
redis.call('PEXPIRE', tokens_key, 7200000)  -- 2h
redis.call('PEXPIRE', ts_key, 7200000)

return {allowed, tokens}
"""
async def token_bucket_allow(redis: Redis, key: str, capacity: int, refill_per_sec: float, cost: float = 1.0) -> Tuple[bool, float]:
    tokens_key = f"rl:tb:tokens:{key}"
    ts_key = f"rl:tb:ts:{key}"
    now_ms = int(time.time() * 1000)
    allowed, tokens = await redis.eval(TOKEN_BUCKET_LUA, 2, tokens_key, ts_key, capacity, refill_per_sec, now_ms, cost)
    return bool(allowed), float(tokens)

api/app/metrics.py (Prometheus)

from prometheus_client import Counter, Histogram, CONTENT_TYPE_LATEST, generate_latest
from starlette.responses import Response
REQUESTS_TOTAL = Counter(
    "requests_total", "Total API requests", ["endpoint", "status", "strategy"]
)
RATE_LIMITED_TOTAL = Counter(
    "rate_limited_total", "Rate-limited responses", ["strategy"]
)
REQUEST_LATENCY = Histogram(
    "request_latency_seconds", "Latency of requests in seconds"
)
async def metrics_endpoint():
    return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)

api/app/main.py (endpoints & middleware)

from fastapi import FastAPI, Depends, Query, Request
from pydantic import BaseModel
import os, time
from typing import Optional
from redis.asyncio import Redis
from .redis_client import get_redis
from .ratelimit import fixed_window_allow, sliding_window_allow, token_bucket_allow
from .metrics import REQUESTS_TOTAL, RATE_LIMITED_TOTAL, REQUEST_LATENCY, metrics_endpoint
app = FastAPI(title="Redis A-to-Z: Atomic Ops & Rate Limiting + Metrics")

Latency middleware (correct HTTP style)

@app.middleware("http")
async def track_latency(request: Request, call_next):
    start = time.perf_counter()
    response = await call_next(request)
    REQUEST_LATENCY.observe(time.perf_counter() - start)
    return response
@app.get("/metrics")
async def metrics():
    return await metrics_endpoint()
@app.get("/health")
async def health(redis: Redis = Depends(get_redis)):
    pong = await redis.ping()
    return {"ok": pong}

Composite identity helper

def make_composite_id(request: Request, api_key: Optional[str]) -> str:
    ip = request.headers.get("x-forwarded-for")
    if ip:
        ip = ip.split(",")[0].strip()
    else:
        ip = request.client.host if request.client else "unknown"
    key = api_key or "anon"
    return f"{key}:{ip}"

INCR demo

@app.get("/counter/incr")
async def incr_counter(key: str = Query("page_views"), redis: Redis = Depends(get_redis)):
    new_val = await redis.incr(key, 1)
    REQUESTS_TOTAL.labels(endpoint="/counter/incr", status="200", strategy="na").inc()
    return {"key": key, "new_value": new_val}

@app.get("/counter/get")
async def get_counter(key: str = Query("page_views"), redis: Redis = Depends(get_redis)):
    val = await redis.get(key)
    REQUESTS_TOTAL.labels(endpoint="/counter/get", status="200", strategy="na").inc()
    return {"key": key, "value": int(val) if val is not None else 0}

Stock transfer (WATCH/MULTI/EXEC)

class Transfer(BaseModel):
    from_item: str
    to_item: str
    qty: int
@app.post("/stock/seed")
async def seed_stock(item: str = Query(...), qty: int = Query(100), redis: Redis = Depends(get_redis)):
    await redis.set(f"stock:{item}", qty)
    REQUESTS_TOTAL.labels(endpoint="/stock/seed", status="200", strategy="na").inc()
    return {"seeded": {item: qty}}
@app.get("/stock/get")
async def get_stock(item: str, redis: Redis = Depends(get_redis)):
    v = await redis.get(f"stock:{item}")
    REQUESTS_TOTAL.labels(endpoint="/stock/get", status="200", strategy="na").inc()
    return {"item": item, "qty": int(v) if v else 0}
@app.post("/stock/transfer")
async def transfer_stock(body: Transfer, redis: Redis = Depends(get_redis)):
    s_from = f"stock:{body.from_item}"
    s_to   = f"stock:{body.to_item}"

    async with redis.pipeline(transaction=True) as pipe:
        while True:
            try:
                await pipe.watch(s_from, s_to)        # optimistic lock
                from_qty_raw = await redis.get(s_from)
                from_qty = int(from_qty_raw) if from_qty_raw else 0
                if from_qty < body.qty:
                    await pipe.reset()
                    REQUESTS_TOTAL.labels(endpoint="/stock/transfer", status="200", strategy="na").inc()
                    return {"ok": False, "reason": "insufficient stock"}

                pipe.multi()
                pipe.decrby(s_from, body.qty)         # debit A
                pipe.incrby(s_to, body.qty)           # credit B
                await pipe.execute()                   # commit atomically
                break
            except Exception:
                continue                               # retry on conflict
            finally:
                await pipe.reset()

    a = int(await redis.get(s_from) or 0)
    b = int(await redis.get(s_to) or 0)
    REQUESTS_TOTAL.labels(endpoint="/stock/transfer", status="200", strategy="na").inc()
    return {"ok": True, "from": {body.from_item: a}, "to": {body.to_item: b}}

Lua wallet (single-round-trip atomic debit)

DEBIT_LUA = """
local key = KEYS[1]
local amount = tonumber(ARGV[1])
local bal = tonumber(redis.call('GET', key) or '0')
if bal >= amount then
  redis.call('DECRBY', key, amount)
  return {1, bal - amount}
else
  return {0, bal}
end
"""
@app.post("/wallet/seed")
async def wallet_seed(user: str, amount: int = 100, redis: Redis = Depends(get_redis)):
    await redis.set(f"wallet:{user}", amount)
    REQUESTS_TOTAL.labels(endpoint="/wallet/seed", status="200", strategy="na").inc()
    return {"user": user, "balance": amount}

@app.post("/wallet/debit")
async def wallet_debit(user: str, amount: int, redis: Redis = Depends(get_redis)):
    key = f"wallet:{user}"
    ok, new_bal = await redis.eval(DEBIT_LUA, 1, key, amount)
    REQUESTS_TOTAL.labels(endpoint="/wallet/debit", status="200", strategy="na").inc()
    return {"ok": bool(ok), "user": user, "balance": int(new_bal)}

Rate limiters

All three use composite ID so demo123@1.2.3.4 is separate from demo123@5.6.7.8 or anon@ip.

@app.get("/rate/fixed")
async def rate_fixed(request: Request, user: Optional[str] = None, api_key: Optional[str] = None, redis: Redis = Depends(get_redis)):
    limit = int(os.getenv("FIXED_WINDOW_LIMIT", "5"))
    secs  = int(os.getenv("FIXED_WINDOW_SECONDS", "10"))
    key = user or make_composite_id(request, api_key or request.headers.get("x-api-key"))
    allowed, remaining, ttl = await fixed_window_allow(redis, key, limit, secs)
    status = "200" if allowed else "429"
    REQUESTS_TOTAL.labels(endpoint="/rate/fixed", status=status, strategy="fixed").inc()
    if not allowed:
        RATE_LIMITED_TOTAL.labels(strategy="fixed").inc()
    return {"strategy": "fixed", "key": key, "allowed": allowed, "remaining": remaining, "retry_in": ttl, "limit": limit, "window_seconds": secs}
@app.get("/rate/sliding")
async def rate_sliding(request: Request, user: Optional[str] = None, api_key: Optional[str] = None, redis: Redis = Depends(get_redis)):
    limit = int(os.getenv("SLIDING_WINDOW_LIMIT", "8"))
    secs  = int(os.getenv("SLIDING_WINDOW_SECONDS", "10"))
    key = user or make_composite_id(request, api_key or request.headers.get("x-api-key"))
    allowed, remaining = await sliding_window_allow(redis, key, limit, secs)
    status = "200" if allowed else "429"
    REQUESTS_TOTAL.labels(endpoint="/rate/sliding", status=status, strategy="sliding").inc()
    if not allowed:
        RATE_LIMITED_TOTAL.labels(strategy="sliding").inc()
    return {"strategy": "sliding", "key": key, "allowed": allowed, "remaining": remaining, "limit": limit, "window_seconds": secs}
@app.get("/rate/tokenbucket")
async def rate_tokenbucket(
    request: Request,
    user: Optional[str] = None,
    api_key: Optional[str] = None,
    capacity: int = Query(10, description="Max tokens"),
    refill_per_sec: float = Query(2.0, description="Tokens added per second"),
    cost: float = Query(1.0, description="Tokens required per request"),
    redis: Redis = Depends(get_redis)
):
    key = user or make_composite_id(request, api_key or request.headers.get("x-api-key"))
    allowed, tokens_left = await token_bucket_allow(redis, key, capacity, refill_per_sec, cost)
    status = "200" if allowed else "429"
    REQUESTS_TOTAL.labels(endpoint="/rate/tokenbucket", status=status, strategy="tokenbucket").inc()
    if not allowed:
        RATE_LIMITED_TOTAL.labels(strategy="tokenbucket").inc()
    return {"strategy": "tokenbucket", "key": key, "allowed": allowed, "tokens_left": tokens_left, "capacity": capacity, "refill_per_sec": refill_per_sec, "cost": cost}

api/app/scripts/load_test.py (parallel tester)

import asyncio, aiohttp, sys
async def hit(url: str, i: int):
    async with aiohttp.ClientSession() as sess:
        async with sess.get(url) as r:
            js = await r.json()
            ok = js.get("allowed")
            print(f"{i:02d}: {'OK' if ok else 'BLOCKED'} -> {js}")
async def main():
    url = sys.argv[1]
    n = int(sys.argv[2])
    await asyncio.gather(*[hit(url, i) for i in range(n)])
if __name__ == "__main__":
    asyncio.run(main())

prometheus/prometheus.yml

global:
  scrape_interval: 5s
scrape_configs:
  - job_name: "api"
    static_configs:
      - targets: ["api:8001"]
    metrics_path: /metrics

grafana provisioning

datasources/datasource.yml

apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true

dashboards/dashboards.yml

apiVersion: 1
providers:
  - name: 'RateLimiterDashboards'
    orgId: 1
    folder: ''
    type: file
    options:
      path: /var/lib/grafana/dashboards

dashboards/redisratelimiter.json


Tests & Why They Work (quick recap)


GRAFANA

Perfect โ€” youโ€™ve reached the most rewarding stage ๐ŸŽ‰ โ€” seeing Redis limits and Prometheus metrics come alive inside Grafana. Letโ€™s go step-by-step so you know exactly how to open Grafana, explore, and interpret each panel.


๐Ÿงญ 1. Start Everything

From your project root:

docker compose up --build

Wait until you see these in logs:

grafana-1  | t=... level=info msg="Starting Grafana" ...
api-1      | INFO: Uvicorn running on http://0.0.0.0:8001

That means all services (Redis, FastAPI, Prometheus, Grafana) are live.


๐ŸŒ 2. Open Grafana

Open a browser and visit:

๐Ÿ‘‰ http://localhost:3000

Default credentials:

User Password
admin admin

Youโ€™ll be prompted to set a new password (optional โ€” you can skip for local dev).


๐Ÿ“Š 3. Check the Data Source Connection

Grafana should already have Prometheus pre-configured via provisioning, but verify:

  1. Left sidebar โ†’ โš™๏ธ (Gear icon) โ†’ Data Sources
  2. You should see Prometheus

  3. If itโ€™s red, click Save & Test once.


๐Ÿ—บ๏ธ 4. Open the Pre-Loaded Dashboard

  1. Left sidebar โ†’ Dashboards โ†’ Browse
  2. Look for a dashboard named โ€œRedis Rate Limiter (API)โ€
  3. Click to open it.

If you donโ€™t see it immediately:


๐ŸŽ›๏ธ 5. Interpreting Each Panel

Once you start hitting API endpoints (with curl, browser, or load_test.py), youโ€™ll see live data populate.

๐Ÿ”น Panel 1 โ€” Total Requests (5 min)

Query: sum(increase(requests_total[5m]))

Shows how many total requests were received in the last 5 minutes across all endpoints.

โžก๏ธ Try running:

for i in {1..10}; do curl -s http://localhost:8001/rate/fixed; done

Watch the counter rise.


๐Ÿ”น Panel 2 โ€” Total Rate-Limited (5 min)

Query: sum(increase(rate_limited_total[5m]))

Shows how many requests were throttled (HTTP 429) in the past 5 minutes. Hit an endpoint repeatedly to trigger it:

for i in {1..10}; do curl -s http://localhost:8001/rate/fixed; done

Once you exceed 5/10 s, youโ€™ll see this panel climb.


๐Ÿ”น Panel 3 โ€” Requests by Endpoint & Status (1 m rate)

Query: sum by (endpoint,status) (rate(requests_total[1m]))

Plots a time series of request rate (requests/sec) per endpoint and HTTP status.

This lets you visually confirm how limits throttle bursts.


๐Ÿ”น Panel 4 โ€” Rate-Limited by Strategy (1 m rate)

Query: sum by (strategy) (rate(rate_limited_total[1m]))

Shows which strategy (fixed/sliding/tokenbucket) is currently blocking most traffic. Great for comparing fairness between algorithms.


๐Ÿ”น Panel 5 โ€” Latency p50 / p90 / p99

Queries:

histogram_quantile(0.5, sum(rate(request_latency_seconds_bucket[1m])) by (le))
histogram_quantile(0.9, ...)
histogram_quantile(0.99, ...)

Displays median, 90th, and 99th percentile response times in seconds. If you simulate heavy load, youโ€™ll notice p99 rising slightly.


๐Ÿ”„ 6. Adjust Time Range & Refresh

Top-right corner in Grafana:

This continuously updates your panels as Prometheus scrapes.


๐Ÿง  7. Optional: Explore Metrics Directly

Click Explore (compass icon ๐Ÿงญ):


โš™๏ธ 8. Customize Panels

You can clone and tweak panels:

All edits can be saved to a new dashboard or the same one.


๐Ÿงฉ 9. Quick Troubleshooting

Symptom Fix
Grafana opens but panels empty Wait 10โ€“15 s; ensure youโ€™ve called API endpoints to generate data.
โ€œData source errorโ€ Open Prometheus UI at http://localhost:9090 โ†’ Status > Targets; verify api:8001 is UP.
No dashboard found Import the JSON manually from grafana/dashboards/redis_rate_limiter.json.
All graphs flat = 0 Check that /metrics is reachable: curl [http://localhost:8001/metrics](http://localhost:8001/metrics) | head.

๐ŸŽ‰ 10. When Everything Works

Youโ€™ll see, in Grafana:

Congratulations ๐ŸŽŠ โ€” youโ€™re now watching Redis, Prometheus, and Grafana in perfect sync!


Perfect ๐ŸŽฌ โ€” hereโ€™s your ready-to-record YouTube walkthrough script (about 2ยฝ minutes). Itโ€™s written as a teleprompter-style narration โ€” friendly, confident, and perfectly suited for your TechieTalksAI video voiceover.


๐ŸŽฅ YouTube Teleprompter Script

Title: Redis + FastAPI + Grafana: Watch Rate Limiting Come Alive!


๐ŸŽ™๏ธ [Opening โ€“ 0:00-0:20]

โ€œWelcome to TechieTalksAI! In this quick walkthrough, youโ€™ll see how our Redis-powered FastAPI application tracks requests, enforces rate limits, and visualizes everything in real time using Prometheus and Grafana โ€” all running inside Docker.โ€

โ€œLetโ€™s open Grafana and see the system in action!โ€


๐ŸŽ™๏ธ [Step 1 โ€“ Launch Grafana โ€“ 0:20-0:40]

โ€œOnce Docker is up, open your browser and go to localhost:3000. Login with the default username admin and password admin. Grafana starts with a pre-configured Prometheus data source that collects all metrics from our FastAPI app automatically.โ€


๐ŸŽ™๏ธ [Step 2 โ€“ Open the Dashboard โ€“ 0:40-1:00]

โ€œNext, on the left panel, click Dashboards and open โ€˜Redis Rate Limiter (API)โ€™. Youโ€™ll see several live panels โ€” Total Requests, Rate-Limited Requests, Endpoint Metrics, and Latency Percentiles โ€” all updating in real time as our API runs.โ€


๐ŸŽ™๏ธ [Step 3 โ€“ Generate Live Data โ€“ 1:00-1:40]

โ€œLetโ€™s create some activity. In a terminal, run a small curl loop hitting our /rate/fixed endpoint.โ€

(show terminal)

for i in {1..10}; do curl -s http://localhost:8001/rate/fixed; done

โ€œWatch what happens โ€” the Total Requests chart jumps immediately. When you cross the fixed-window limit, new calls return 429 Too Many Requests. See that red line in the Rate-Limited chart? Thatโ€™s Redis instantly throttling extra calls!โ€


๐ŸŽ™๏ธ [Step 4 โ€“ Explore the Dashboard โ€“ 1:40-2:10]

โ€œEach panel tells a story:


๐ŸŽ™๏ธ [Step 5 โ€“ Wrap-Up โ€“ 2:10-2:40]

โ€œIn just a few minutes, we built a complete observability loop: FastAPI collects metrics โ†’ Prometheus scrapes them โ†’ Grafana visualizes them. Every spike you see here corresponds to a real Redis atomic operation happening inside Docker!โ€

โ€œWant to try it yourself? The full Docker Compose and code are linked below. If you found this demo useful, donโ€™t forget to like, subscribe, and share โ€” Iโ€™m Sreeprakash from TechieTalksAI โ€” helping you build smarter systems with AI, automation, and cloud!โ€


๐ŸŽฏ Suggested On-Screen Captions & Visual Cues

Time Visual
0:00-0:10 โ€œRedis + FastAPI + Grafana โ€” Real-time Rate Limiting!โ€ overlay
0:40 Screen capture of Grafana login and dashboard list
1:00 Terminal sending curl requests
1:20 Grafana charts animating spikes
2:10 Split view: Redis Commander + Grafana + terminal
2:35 โ€œSubscribe to TechieTalksAIโ€ outro banner

Prometheus

Excellent question โ€” Prometheus is the invisible engine that makes your Grafana charts alive. Letโ€™s go through it thoroughly but clearly, step by step, so you understand what Prometheus is, how it fits your Redis-FastAPI system, and how you can browse and query it directly.


๐Ÿ”ฅ What Prometheus Is (In Simple Terms)

Prometheus is an open-source time-series database built specifically for metrics and monitoring.

It doesnโ€™t store logs or traces โ€” only numeric measurements over time, like:

Every metric is stored with labels (like endpoint="/rate/fixed", status="200", strategy="tokenbucket") that let you query and group data flexibly.

So you can ask questions like:

โ€œHow many /rate/fixed requests got 429 errors in the last 5 minutes?โ€

Prometheus collects this data by scraping HTTP endpoints โ€” usually at /metrics โ€” that return metrics in a simple text format.


๐Ÿงฉ How Prometheus Fits into Your Stack

In your Docker setup, the pieces connect like this:

FastAPI  โ†’ exposes /metrics via prometheus_client
Prometheus โ†’ scrapes /metrics every 5 seconds
Grafana   โ†’ queries Prometheus and draws charts

So Prometheus acts as the data collector and historian โ€” the middleman between FastAPI and Grafana.


โš™๏ธ Configuration Breakdown

Inside your project you have prometheus/prometheus.yml:

global:
  scrape_interval: 5s
scrape_configs:
  - job_name: "api"
    static_configs:
      - targets: ["api:8001"]
    metrics_path: /metrics

Prometheus automatically reaches out to http://api:8001/metrics every 5 seconds and records every metric name/value it sees.


๐Ÿงฎ What Itโ€™s Actually Collecting

Your FastAPI app uses prometheus_client and exports these variables:

Metric Type Labels Meaning
requests_total Counter endpoint, status, strategy Counts total requests per endpoint/status
rate_limited_total Counter strategy Counts blocked requests
request_latency_seconds Histogram none Tracks request durations in buckets

Every time a request is handled, the app increments or observes these metrics. Then Prometheus scrapes them and stores a timestamped value like:

requests_total{endpoint="/rate/fixed",status="200",strategy="fixed"} 57
requests_total{endpoint="/rate/fixed",status="429",strategy="fixed"} 8
request_latency_seconds_bucket{le="0.5"} 61

๐Ÿงญ Browsing Prometheus Directly

You can open its web UI anytime at:

๐Ÿ‘‰ http://localhost:9090

Youโ€™ll see a simple search box labeled โ€œExpressionโ€.

Try typing these and hitting Execute:

1. requests_total

Lists all request counters and their label combinations.

2. rate(requests_total[1m])

Shows per-second request rate averaged over the last 1 minute.

3. sum(rate(rate_limited_total[1m]))

Aggregates how many requests were blocked per second recently.

4. histogram_quantile(0.9, sum(rate(request_latency_seconds_bucket[1m])) by (le))

Calculates 90th-percentile latency.


Bonus: Status โ†’ Targets

Click Status โ†’ Targets (top menu).

You should see:

Job Endpoint State
api api:8001/metrics UP โœ…

If itโ€™s DOWN, Prometheus couldnโ€™t reach the FastAPI service โ€” fix networking or the endpoint.


๐Ÿง  How Prometheus Stores Data

For example:

increase(requests_total[5m])

โ†’ โ€œHow much did this counter increase in the last 5 minutes?โ€


๐Ÿ”ฌ Example: Live Querying

Letโ€™s generate some requests:

for i in {1..15}; do curl -s http://localhost:8001/rate/tokenbucket; done

Now in Prometheus UI:

sum(rate(requests_total{strategy="tokenbucket"}[30s]))

Youโ€™ll see a numeric value โ€” thatโ€™s the requests per second for tokenbucket endpoint, measured over the last 30 seconds.


๐Ÿ’พ Why Prometheus Is So Important

Feature Benefit
Pull-based No need to push metrics; Prometheus fetches them safely.
Time-series DB Perfect for โ€œhow has this changed over time?โ€
Labels system Lets you drill down and aggregate with precision.
Alerting rules You can define alerts like โ€œtoo many 429s per minuteโ€.
Integrates with Grafana Makes data visually clear for analysis and demos.

In your system, Prometheus makes Redisโ€™s real-time behavior measurable and explainable.

Without it, Grafana would have no live data โ€” Redis operations would remain a black box.


๐Ÿงญ Summary


Perfect ๐ŸŽ™๏ธ Hereโ€™s your 1-minute TechieTalksAI voice-over script explaining Prometheus โ€” short, sharp, visual, and fits naturally as a mid-section segment inside your Redis + Grafana video.


๐ŸŽฅ Teleprompter Script โ€“ โ€œPrometheus: The Silent Data Engineโ€ (โ‰ˆ 60 seconds)

๐ŸŽ™๏ธ [Start โ€“ 0:00 โ€“ 0:10] โ€œNow letโ€™s talk about the silent brain behind these live charts โ€” Prometheus. Itโ€™s the system that continuously collects, stores, and organizes all our API metrics.โ€


๐ŸŽ™๏ธ [0:10 โ€“ 0:25] โ€œEvery few seconds, Prometheus scrapes our FastAPI endpoint /metrics, pulling raw numbers like total requests, rate-limited calls, and latency samples. Each value gets labeled โ€” endpoint, status, strategy โ€” and stored as a time-series, so we can rewind and see how traffic changed over time.โ€

(show the Prometheus UI at localhost:9090 and a quick query like requests_total)


๐ŸŽ™๏ธ [0:25 โ€“ 0:40] โ€œIt doesnโ€™t wait for the app to send data โ€” Prometheus goes and fetches it. That pull-based model makes it super reliable and safe, even if one service crashes or restarts โ€” Prometheus keeps the history intact.โ€

(cut to Grafana graphs updating while Prometheus target list shows โ€˜UPโ€™)


๐ŸŽ™๏ธ [0:40 โ€“ 0:55] โ€œGrafana then queries Prometheus using its powerful PromQL language. Those lines and bars you see? Theyโ€™re live math over the Prometheus database โ€” turning plain metrics into beautiful, readable insights.โ€


๐ŸŽ™๏ธ [0:55 โ€“ 1:00] โ€œSo next time you see a dashboard spike, remember: Prometheus is the heartbeat recorder of your entire API ecosystem.โ€


๐ŸŽฌ Optional On-Screen Cues

Time Visual
0:05 Prometheus logo + โ€œTime-Series Database for Metricsโ€
0:15 /metrics endpoint text output scrolling
0:25 Prometheus web UI query box with requests_total
0:35 Target list showing โ€œapi:8001 โ€“ UP โœ…โ€
0:45 Grafana dashboard animating
0:58 Fade-in tagline: โ€œPrometheus โ€“ Metrics that Matter.โ€


๐Ÿ Outro

This project demonstrates not just how Redis works โ€” but why itโ€™s indispensable for modern backends.

You now have:

Use this as your personal Redis lab. Extend it to:

With this, youโ€™ve mastered Redis as the brain behind intelligent, observable, and self-protecting APIs.



OLD

A fully self-contained, Docker-Compose quickstart that teaches Redis from zero to โ€œI built a rate limiter,โ€ with runnable code, clear endpoints, and easy curl tests. It includes:


๐Ÿš€ What youโ€™ll build


๐Ÿงฑ Project layout

redis-redislim/
โ”œโ”€ docker-compose.yml
โ”œโ”€ .env
โ”œโ”€ api/
โ”‚  โ”œโ”€ Dockerfile
โ”‚  โ”œโ”€ requirements.txt
โ”‚  โ””โ”€ app/
โ”‚     โ”œโ”€ main.py
โ”‚     โ”œโ”€ redis_client.py
โ”‚     โ”œโ”€ ratelimit.py
โ”‚     โ””โ”€ scripts/
โ”‚        โ””โ”€ load_test.py

๐Ÿ“ฆ Files

1) docker-compose.yml

version: "3.9"

services:
  redis:
    image: redis:7-alpine
    command: ["redis-server", "--appendonly", "yes", "--save", "60", "1000"]
    ports:
      - "6379:6379"
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "PING"]
      interval: 5s
      timeout: 3s
      retries: 20

  redis-commander:
    image: rediscommander/redis-commander:latest
    environment:
      - REDIS_HOSTS=local:redis:6379
    ports:
      - "8081:8081"
    depends_on:
      - redis

  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    env_file:
      - .env
    depends_on:
      redis:
        condition: service_healthy
    ports:
      - "8001:8001"
    volumes:
      - ./api/app:/app

volumes:
  redisdata:

2) .env

REDIS_URL=redis://redis:6379/0
APP_HOST=0.0.0.0
APP_PORT=8001
FIXED_WINDOW_LIMIT=5
FIXED_WINDOW_SECONDS=10
SLIDING_WINDOW_LIMIT=8
SLIDING_WINDOW_SECONDS=10

3) api/Dockerfile

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# copy code (compose also binds the folder for hot-reload)
COPY app ./app

EXPOSE 8001

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001", "--reload"]

4) api/requirements.txt

fastapi==0.115.0
uvicorn==0.30.6
redis==5.0.7
pydantic==2.9.2

5) api/app/redis_client.py

from redis.asyncio import Redis
import os

_redis: Redis | None = None

async def get_redis() -> Redis:
    global _redis
    if _redis is None:
        url = os.getenv("REDIS_URL", "redis://localhost:6379/0")
        _redis = Redis.from_url(url, encoding="utf-8", decode_responses=True)
    return _redis

6) api/app/ratelimit.py

import time
from typing import Tuple
from redis.asyncio import Redis

# ---------- Fixed Window (INCR + EXPIRE) ----------
async def fixed_window_allow(redis: Redis, key: str, limit: int, window_secs: int) -> Tuple[bool, int, int]:
    """
    Returns: (allowed, remaining, ttl)
    Key pattern: rl:fixed:{key}
    """
    rk = f"rl:fixed:{key}"
    async with redis.pipeline(transaction=True) as pipe:
        while True:
            try:
                await pipe.watch(rk)
                # If key doesn't exist, start a new window
                exists = await redis.exists(rk)
                pipe.multi()
                pipe.incr(rk, 1)
                if not exists:
                    pipe.expire(rk, window_secs)
                res = await pipe.execute()
                # res[0] is new count, res[1] is expire set/remaining bool possibly None
                new_count = res[0]
                ttl = await redis.ttl(rk)
                allowed = new_count <= limit
                remaining = max(limit - new_count, 0)
                return allowed, remaining, ttl
            except Exception:
                # Retry if concurrent modification
                continue
            finally:
                await pipe.reset()

# ---------- Sliding Window (ZSET timestamps) ----------
async def sliding_window_allow(redis: Redis, key: str, limit: int, window_secs: int) -> Tuple[bool, int]:
    """
    Returns: (allowed, remaining)
    Uses ZSET with timestamps, evicts older-than-window items.
    Key pattern: rl:slide:{key}
    """
    rk = f"rl:slide:{key}"
    now_ms = int(time.time() * 1000)
    window_start_ms = now_ms - window_secs * 1000

    async with redis.pipeline(transaction=True) as pipe:
        # remove old
        pipe.zremrangebyscore(rk, 0, window_start_ms)
        # add now
        pipe.zadd(rk, {str(now_ms): now_ms})
        # count
        pipe.zcard(rk)
        # set an expire so stale keys go away eventually
        pipe.expire(rk, window_secs * 2)
        _, _, count, _ = await pipe.execute()

    allowed = count <= limit
    remaining = max(limit - count, 0)
    return allowed, remaining

7) api/app/main.py

from fastapi import FastAPI, Depends, Query
from pydantic import BaseModel
import os
from typing import Optional
from redis.asyncio import Redis
from .redis_client import get_redis
from .ratelimit import fixed_window_allow, sliding_window_allow

app = FastAPI(title="Redis A-to-Z: Atomic Ops & Rate Limiting")

@app.get("/health")
async def health(redis: Redis = Depends(get_redis)):
    pong = await redis.ping()
    return {"ok": pong}

# -------------------------------------------------------
# INCR demo
# -------------------------------------------------------
@app.get("/counter/incr")
async def incr_counter(
    key: str = Query("page_views", description="Counter key"),
    redis: Redis = Depends(get_redis)
):
    new_val = await redis.incr(key, 1)  # atomic increment
    return {"key": key, "new_value": new_val}

@app.get("/counter/get")
async def get_counter(
    key: str = Query("page_views"),
    redis: Redis = Depends(get_redis)
):
    val = await redis.get(key)
    return {"key": key, "value": int(val) if val is not None else 0}

# -------------------------------------------------------
# Atomic stock transfer (MULTI/EXEC)
# -------------------------------------------------------
class Transfer(BaseModel):
    from_item: str
    to_item: str
    qty: int

@app.post("/stock/seed")
async def seed_stock(
    item: str = Query(...),
    qty: int = Query(100),
    redis: Redis = Depends(get_redis)
):
    await redis.set(f"stock:{item}", qty)
    return {"seeded": {item: qty}}

@app.get("/stock/get")
async def get_stock(
    item: str,
    redis: Redis = Depends(get_redis)
):
    v = await redis.get(f"stock:{item}")
    return {"item": item, "qty": int(v) if v else 0}

@app.post("/stock/transfer")
async def transfer_stock(
    body: Transfer,
    redis: Redis = Depends(get_redis)
):
    s_from = f"stock:{body.from_item}"
    s_to   = f"stock:{body.to_item}"

    async with redis.pipeline(transaction=True) as pipe:
        while True:
            try:
                await pipe.watch(s_from, s_to)
                from_qty_raw = await redis.get(s_from)
                from_qty = int(from_qty_raw) if from_qty_raw else 0
                if from_qty < body.qty:
                    await pipe.reset()
                    return {"ok": False, "reason": "insufficient stock"}

                pipe.multi()
                pipe.decrby(s_from, body.qty)
                pipe.incrby(s_to, body.qty)
                await pipe.execute()
                break
            except Exception:
                continue
            finally:
                await pipe.reset()

    a = int(await redis.get(s_from) or 0)
    b = int(await redis.get(s_to) or 0)
    return {"ok": True, "from": {body.from_item: a}, "to": {body.to_item: b}}

# -------------------------------------------------------
# Lua script demo (single-round-trip atomic check-decrement)
# -------------------------------------------------------
DEBIT_LUA = """
local key = KEYS[1]
local amount = tonumber(ARGV[1])
local bal = tonumber(redis.call('GET', key) or '0')
if bal >= amount then
  redis.call('DECRBY', key, amount)
  return {1, bal - amount}
else
  return {0, bal}
end
"""

@app.post("/wallet/seed")
async def wallet_seed(user: str, amount: int = 100, redis: Redis = Depends(get_redis)):
    await redis.set(f"wallet:{user}", amount)
    return {"user": user, "balance": amount}

@app.post("/wallet/debit")
async def wallet_debit(user: str, amount: int, redis: Redis = Depends(get_redis)):
    key = f"wallet:{user}"
    ok, new_bal = await redis.eval(DEBIT_LUA, 1, key, amount)
    return {"ok": bool(ok), "user": user, "balance": int(new_bal)}

# -------------------------------------------------------
# Fixed-window rate limit
# -------------------------------------------------------
@app.get("/rate/fixed")
async def rate_fixed(
    user: str,
    redis: Redis = Depends(get_redis)
):
    limit = int(os.getenv("FIXED_WINDOW_LIMIT", "5"))
    secs  = int(os.getenv("FIXED_WINDOW_SECONDS", "10"))
    allowed, remaining, ttl = await fixed_window_allow(redis, user, limit, secs)
    status = "OK" if allowed else "RATE_LIMITED"
    return {"strategy": "fixed", "user": user, "allowed": allowed, "remaining": remaining, "retry_in": ttl, "limit": limit, "window_seconds": secs, "status": status}

# -------------------------------------------------------
# Sliding-window rate limit
# -------------------------------------------------------
@app.get("/rate/sliding")
async def rate_sliding(
    user: str,
    redis: Redis = Depends(get_redis)
):
    limit = int(os.getenv("SLIDING_WINDOW_LIMIT", "8"))
    secs  = int(os.getenv("SLIDING_WINDOW_SECONDS", "10"))
    allowed, remaining = await sliding_window_allow(redis, user, limit, secs)
    status = "OK" if allowed else "RATE_LIMITED"
    return {"strategy": "sliding", "user": user, "allowed": allowed, "remaining": remaining, "limit": limit, "window_seconds": secs, "status": status}

8) api/app/scripts/load_test.py

import asyncio
import aiohttp
import sys

# Usage:
#   python scripts/load_test.py http://localhost:8001/rate/fixed?user=sree  10
# or:
#   python scripts/load_test.py http://localhost:8001/rate/sliding?user=sree  15

async def hit(url: str, i: int):
    async with aiohttp.ClientSession() as sess:
        async with sess.get(url) as r:
            js = await r.json()
            ok = js.get("allowed")
            print(f"{i:02d}: {'OK' if ok else 'BLOCKED'} -> {js}")

async def main():
    url = sys.argv[1]
    n = int(sys.argv[2])
    await asyncio.gather(*[hit(url, i) for i in range(n)])

if __name__ == "__main__":
    asyncio.run(main())

Add to requirements for the script (optional): aiohttp==3.10.5. If you want to run the load test inside the API container, add aiohttp to requirements.txt. Otherwise run it from your host Python environment.


โ–ถ๏ธ Run it

# 1) start everything
docker compose up --build

# 2) Open:
# API:              http://localhost:8001/docs
# Redis-Commander:  http://localhost:8081  (Keys appear live while you test!)

๐Ÿงช Try these curl tests (copyโ€“paste)

Health

curl -s http://localhost:8001/health

INCR (atomic counter)

curl -s "http://localhost:8001/counter/incr?key=page_views"
curl -s "http://localhost:8001/counter/get?key=page_views"

Watch page_views change in Redis-Commander under key page_views.

Atomic stock transfer (MULTI/EXEC)

curl -s "http://localhost:8001/stock/seed?item=a&qty=50"
curl -s "http://localhost:8001/stock/seed?item=b&qty=10"
curl -s "http://localhost:8001/stock/transfer" -X POST -H "Content-Type: application/json" -d '{"from_item":"a","to_item":"b","qty":7}'
curl -s "http://localhost:8001/stock/get?item=a"
curl -s "http://localhost:8001/stock/get?item=b"

This uses watch-retry + multi/exec so concurrent transfers remain correct.

Lua = single-round-trip atomic debit

curl -s "http://localhost:8001/wallet/seed?user=sree&amount=100" -X POST
curl -s "http://localhost:8001/wallet/debit?user=sree&amount=30" -X POST
curl -s "http://localhost:8001/wallet/debit?user=sree&amount=100" -X POST

The second debit may fail if balance insufficient; logic is entirely atomic inside Redis.

Rate limiting โ€“ Fixed window (INCR + EXPIRE)

# Try ~7 hits quickly; default is limit=5 / 10s
for i in {1..7}; do curl -s "http://localhost:8001/rate/fixed?user=sree" | jq '.status,.remaining,.retry_in'; done

Observe that after ~5 hits in <10s, youโ€™ll get RATE_LIMITED until TTL resets.

Rate limiting โ€“ Sliding window (fairer)

# Try ~12 hits quickly; default is limit=8 / 10s
for i in {1..12}; do curl -s "http://localhost:8001/rate/sliding?user=sree" | jq '.status,.remaining'; done

Requests become allowed again as timestamps slide out of the last 10 seconds.

Load-test script (optional)

From your host:

python api/app/scripts/load_test.py "http://localhost:8001/rate/fixed?user=sree" 12
python api/app/scripts/load_test.py "http://localhost:8001/rate/sliding?user=sree" 15

๐Ÿง  Concept cheat-sheet (fast clarity)


๐Ÿ”ง Tuning knobs (via .env)

Edit .env and rerun docker compose up -d to test different behaviors.


๐Ÿงน Resetting your playground

# Stop & remove containers
docker compose down

# Also clear Redis data (persistent volume)
docker volume rm redis-redislim_redisdata
# Then up again
docker compose up --build

โœ… What you learned (at a glance)

NEXT

Perfect โ€” hereโ€™s your full educational article (for blog, Medium, or YouTube description) explaining every folder, file, and line in this Redis-based Docker Compose project, including a professional intro and outro.


๐Ÿš€ Building a Redis-Powered Rate Limiting & Analytics API โ€” from Zero to Grafana Dashboards

๐ŸŽฏ Introduction

In modern distributed systems, speed and control are everything. When thousands (or millions) of requests hit your backend each second, you need two things:

  1. Instant decision-making โ€“ whether to allow or throttle the request.
  2. Real-time observability โ€“ so you can see whatโ€™s happening under the hood.

This project is your A-to-Z quickstart to master those two capabilities โ€” built entirely using open source tools and Python:

Weโ€™ll not only implement fixed-window, sliding-window, and token bucket rate limiting but also instrument the API for live metrics and visualize them on Grafana.

By the end, youโ€™ll see every request counted, throttled, and charted โ€” from Redis keyspace to Grafana graph.


๐Ÿ“ Project Structure Overview

redis-redislim/
โ”œโ”€ docker-compose.yml
โ”œโ”€ .env
โ”œโ”€ api/
โ”‚  โ”œโ”€ Dockerfile
โ”‚  โ”œโ”€ requirements.txt
โ”‚  โ””โ”€ app/
โ”‚     โ”œโ”€ main.py
โ”‚     โ”œโ”€ redis_client.py
โ”‚     โ”œโ”€ ratelimit.py
โ”‚     โ”œโ”€ metrics.py
โ”‚     โ””โ”€ scripts/
โ”‚        โ””โ”€ load_test.py
โ”œโ”€ prometheus/
โ”‚  โ””โ”€ prometheus.yml
โ””โ”€ grafana/
   โ”œโ”€ provisioning/
   โ”‚  โ”œโ”€ datasources/datasource.yml
   โ”‚  โ””โ”€ dashboards/dashboards.yml
   โ””โ”€ dashboards/redis_rate_limiter.json

๐Ÿงฑ Folder-by-Folder Deep Dive

๐Ÿณ docker-compose.yml

This is the heart of orchestration, spinning up all services:

version: "3.9"

Specifies Compose file format.

services:
  redis:
    image: redis:7-alpine
    command: ["redis-server", "--appendonly", "yes", "--save", "60", "1000"]
    ports:
      - "6379:6379"
  redis-commander:
    image: rediscommander/redis-commander:latest
    environment:
      - REDIS_HOSTS=local:redis:6379
    ports:
      - "8081:8081"
  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    env_file:
      - .env
    ports:
      - "8001:8001"
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
    ports:
      - "9090:9090"
  grafana:
    image: grafana/grafana:latest
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=admin
    ports:
      - "3000:3000"

Finally, a shared volume redisdata persists Redis state between runs.


โš™๏ธ .env

Contains app configuration:

REDIS_URL=redis://redis:6379/0
APP_HOST=0.0.0.0
APP_PORT=8001
FIXED_WINDOW_LIMIT=5
FIXED_WINDOW_SECONDS=10
SLIDING_WINDOW_LIMIT=8
SLIDING_WINDOW_SECONDS=10

๐Ÿ api/Dockerfile

Defines the build for our FastAPI app:

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
EXPOSE 8001
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001", "--reload"]

Line by line:

  1. Use a lightweight Python image.
  2. Set working directory.
  3. Copy dependencies and install them.
  4. Copy the code.
  5. Expose API port.
  6. Run with Uvicorn, enabling hot reload for fast dev cycles.

๐Ÿ“ฆ api/requirements.txt

Lists dependencies:

fastapi==0.115.0
uvicorn==0.30.6
redis==5.0.7
pydantic==2.9.2
aiohttp==3.10.5
prometheus_client==0.20.0

Each one plays a role:


๐Ÿ”Œ api/app/redis_client.py

from redis.asyncio import Redis
import os

_redis: Redis | None = None

async def get_redis() -> Redis:
    global _redis
    if _redis is None:
        url = os.getenv("REDIS_URL", "redis://localhost:6379/0")
        _redis = Redis.from_url(url, encoding="utf-8", decode_responses=True)
    return _redis

A singleton async client. Ensures only one Redis connection per process, automatically configured via .env.


โฑ๏ธ api/app/ratelimit.py

Implements three strategies:

1๏ธโƒฃ Fixed Window

await redis.incr(key)
await redis.expire(key, window_secs)

2๏ธโƒฃ Sliding Window

pipe.zadd(rk, {str(now_ms): now_ms})
pipe.zremrangebyscore(rk, 0, window_start_ms)

3๏ธโƒฃ Token Bucket (Lua Script)

-- Lua pseudocode
tokens = math.min(capacity, tokens + refill)
if tokens >= cost then
  tokens = tokens - cost
  allowed = 1
end

All functions return (allowed, remaining) or (allowed, tokens_left).


๐Ÿ“Š api/app/metrics.py

Adds Prometheus counters:

REQUESTS_TOTAL = Counter("requests_total", "Total API requests", ["endpoint","status","strategy"])
RATE_LIMITED_TOTAL = Counter("rate_limited_total", "Rate-limited responses", ["strategy"])
REQUEST_LATENCY = Histogram("request_latency_seconds", "Latency of requests")

Also defines middleware:

class TrackLatency:
    async def __call__(self, scope, receive, send):
        start = time.perf_counter()
        ...
        REQUEST_LATENCY.observe(duration)

Every requestโ€™s latency gets observed automatically.


๐Ÿง  api/app/main.py

This is the brain of the system.

Imports

from fastapi import FastAPI, Request, Query, Depends
from .ratelimit import fixed_window_allow, sliding_window_allow, token_bucket_allow
from .metrics import REQUESTS_TOTAL, RATE_LIMITED_TOTAL, TrackLatency, metrics_endpoint

Integrates all modules.

Middleware

app.middleware('http')(TrackLatency(app))

Captures latency for every request.

/metrics

Prometheus scrapes metrics from here.

/counter/*

Simple INCR demo for atomic Redis increments.

/stock/*

Shows MULTI/EXEC transaction with optimistic locking.

/wallet/*

Uses Lua for atomic debit with โ€œcheck-then-update.โ€

/rate/*

Implements three rate limit endpoints:

Each:

  1. Extracts user identity โ†’ X-API-Key + client IP.
  2. Calls Redis logic.
  3. Updates Prometheus counters.
  4. Returns structured JSON (allowed, remaining, retry time, etc.).
def make_composite_id(request: Request, api_key: Optional[str]) -> str:
    ip = request.headers.get("x-forwarded-for") or request.client.host
    key = api_key or "anon"
    return f"{key}:{ip}"

This ensures fair limits per user and per IP.


โšก api/app/scripts/load_test.py

An async load tester:

python api/app/scripts/load_test.py "http://localhost:8001/rate/fixed?user=sree" 15

Sends 15 parallel requests to test throttling behavior.


๐Ÿ“ˆ prometheus/prometheus.yml

Scrapes metrics:

scrape_configs:
  - job_name: "api"
    static_configs:
      - targets: ["api:8001"]
    metrics_path: /metrics

Prometheus polls every 5s.


๐Ÿ“‰ grafana/ folder

Auto-provisions Grafana with:

Dashboard panels include:

Accessible at http://localhost:3000.


๐Ÿงช How to Run & Observe

docker compose up --build

URLs:

Service URL Purpose
FastAPI http://localhost:8001/docs API & Swagger
Redis Commander http://localhost:8081 View Redis keys
Prometheus http://localhost:9090 Metrics database
Grafana http://localhost:3000 Visualization UI

Test Commands

Fixed Window:

for i in {1..7}; do curl -s "http://localhost:8001/rate/fixed?user=sree"; done

Sliding Window:

for i in {1..12}; do curl -s "http://localhost:8001/rate/sliding?user=sree"; done

Token Bucket:

for i in {1..15}; do curl -s "http://localhost:8001/rate/tokenbucket?capacity=10&refill_per_sec=2&cost=1"; done

Then visit Grafana to watch metrics update live.


๐ŸŒ Why Redis for Rate Limiting?

Redis provides:

Itโ€™s the go-to backbone for rate limiting, caching, sessions, and leaderboards.


๐ŸŽ“ What You Learned

โœ… Why Redis is ideal for in-memory counters โœ… How atomicity keeps your state consistent โœ… 3 styles of rate limiting (fixed, sliding, token bucket) โœ… How to use Lua for one-shot atomic updates โœ… How to expose Prometheus metrics from FastAPI โœ… How to visualize everything in Grafana


๐Ÿ Outro โ€” Why This Matters

APIs are the highways of digital systems. Without control, they get congested, abused, and invisible. This project teaches you how to:

Redis is your traffic cop, FastAPI your entry gate, and Grafana your control tower. Together, they form a production-ready pattern every modern engineer should master.

Use this project as your personal lab โ€” tweak, break, and rebuild. From here, you can extend it to:

Once you truly see what Redis, Prometheus, and Grafana are doing together โ€” youโ€™ve stepped into the next level of backend engineering. โš™๏ธโœจ