Bootcamp i backendutveckling med FastAPI · Lektion

Rate limiting och skydd mot botmissbruk

Implementera distribuerad rate limiting och throttling med Redis för att hantera toppar och blockera missbrukande klienter.

Lektion 2 av 413 steg

Rate limiting och skydd mot botmissbruk är en gratis lektion i Bootcamp i backendutveckling med FastAPI på CoddyKit. Detta är lektion 2 av 4. Du kan läsa vilka 3 lektioner som helst i den här lärvägen kostnadsfritt i sin helhet – därefter låser CoddyKit PRO upp alla lektioner, plus praktisk övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Den ingår i lärvägen för Bootcamp i backendutveckling med FastAPI, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i Bootcamp i backendutveckling med FastAPI innehåller totalt 4 lektioner.

Varför distribuerad hastighetsbegränsning behövs

En ensam FastAPI-worker som lagrar en räknare för hastighetsbegränsning i processminnet slutar fungera så fort ni skalar horisontellt. Med N Uvicorn-workers bakom en lastbalanserare får en missbrukande klient N gånger den tillåtna mängden, eftersom varje worker räknar självständigt.

  • Mål: en gemensam räknare som alla workers och pods ser.
  • Verktyg: Redis, ett atomiskt minnesbaserat datalager som alla instanser ansluter till.
  • Målsättning: hantera legitima trafiktoppar samtidigt som missbrukande botar stryps eller blockeras.

Under hela lektionen bygger vi först algoritmerna i ren Python och kopplar sedan in dem som FastAPI-beroenden.

Räknare med fast tidsfönster

Den enklaste algoritmen är att dela in tiden i fasta fönster (till exempel intervall på 60 sekunder) och öka en räknare per klient och fönster. När räknaren överskrider gränsen avvisas begäran.

Nedan ser ni en simulering i ren Python, så att ni kan se hur det fungerar utan Redis. Lägg märke till gränsproblemet: en klient kan skicka hela sin tilldelade kvot i slutet av ett fönster och sedan igen i början av nästa, vilket tillfälligt fördubblar den faktiska frekvensen.

import time

class FixedWindow:
    def __init__(self, limit, window_seconds):
        self.limit = limit
        self.window = window_seconds
        self.buckets = {}

    def allow(self, key, now):
        win = int(now // self.window)
        bucket_key = (key, win)
        count = self.buckets.get(bucket_key, 0) + 1
        self.buckets[bucket_key] = count
        return count <= self.limit

limiter = FixedWindow(limit=3, window_seconds=60)
base = 1000.0
for i in range(5):
    ok = limiter.allow('user:42', base + i)
    print(f'request {i+1}: {"ALLOW" if ok else "BLOCK"}')

Logg med glidande tidsfönster

För att eliminera gränsproblemet sparar vi en logg med tidsstämplar per klient och räknar endast de händelser som inträffat inom det efterföljande fönstret räknat från now. Metoden är exakt men minneskrävande: en post per begäran.

Mönstret nedan är exakt det som vi kommer att översätta till en sorterad Redis-uppsättning, där poängen är tidsstämpeln.

import time
from collections import deque

class SlidingLog:
    def __init__(self, limit, window_seconds):
        self.limit = limit
        self.window = window_seconds
        self.logs = {}

    def allow(self, key, now):
        dq = self.logs.setdefault(key, deque())
        cutoff = now - self.window
        while dq and dq[0] <= cutoff:
            dq.popleft()
        if len(dq) < self.limit:
            dq.append(now)
            return True
        return False

limiter = SlidingLog(limit=2, window_seconds=10)
for t in [0, 1, 2, 11, 12]:
    ok = limiter.allow('ip:1.2.3.4', float(t))
    print(f't={t}s -> {"ALLOW" if ok else "BLOCK"}')

Token bucket: hantera trafiktoppar

Fasta och glidande tidsfönster är strikta. För att hantera toppar och samtidigt upprätthålla ett långsiktigt genomsnitt är token bucket standardvalet.

  • Varje klient har en bucket med en kapacitet (maximal topp) och en påfyllnadstakt (tokens per sekund).
  • Varje begäran förbrukar en token. Om bucketen är tom begränsas begäran.
  • En topp på upp till capacity begäranden passerar omedelbart, varefter trafiken jämnas ut till påfyllnadstakten.

Detta är den algoritm vi rekommenderar för publika API:er som möter trafik med legitima men varierande toppar.

class TokenBucket:
    def __init__(self, capacity, refill_per_sec):
        self.capacity = capacity
        self.refill = refill_per_sec
        self.tokens = capacity
        self.last = 0.0

    def allow(self, now):
        elapsed = now - self.last
        self.tokens = min(self.capacity, self.tokens + elapsed * self.refill)
        self.last = now
        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False

bucket = TokenBucket(capacity=5, refill_per_sec=1)
for t in [0, 0, 0, 0, 0, 0, 3]:
    print(f't={t}: {"ALLOW" if bucket.allow(float(t)) else "THROTTLE"}')

Varför atomicitet är viktigt

Med många workers är läs-ändra-skriv för en räknare ett klassiskt race condition: två workers läser count=4, ökar värdet, skriver båda 5, och en begäran som borde ha blockerats släpps igenom.

Redis löser detta eftersom varje kommando är atomiskt, men ett beslut om hastighetsbegränsning kräver vanligtvis flera kommandon (öka, ange utgångstid, jämför). Den robusta lösningen är ett Lua-skript som körs av EVAL: Redis kör hela skriptet atomiskt, utan att någon annan klient kan lägga sig emellan.

Nästa scener visar det Redis-baserade fasta tidsfönstret och token bucket-algoritmen med denna metod.

Fast tidsfönster i Redis med INCR + EXPIRE

Den billigaste distribuerade begränsaren är en Redis-nyckel per klient och tidsfönster. INCR returnerar det nya antalet atomiskt. Vid det första träffet anger vi en TTL som motsvarar fönstret, så att nyckeln rensas automatiskt.

Vi omsluter båda kommandona i ett litet Lua-skript, så att ökningen och utgångstiden utgör en atomisk enhet. Detta är kopplingskod på FastAPI-sidan, inte ett fristående program.

import redis.asyncio as redis

FIXED_WINDOW_LUA = """
local current = redis.call('INCR', KEYS[1])
if current == 1 then
  redis.call('EXPIRE', KEYS[1], ARGV[1])
end
return current
"""

class RedisFixedWindow:
    def __init__(self, client, limit, window):
        self.client = client
        self.limit = limit
        self.window = window
        self.script = client.register_script(FIXED_WINDOW_LUA)

    async def allow(self, identifier: str) -> bool:
        bucket = int(__import__('time').time()) // self.window
        key = f'rl:fw:{identifier}:{bucket}'
        count = await self.script(keys=[key], args=[self.window])
        return count <= self.limit

# rl = RedisFixedWindow(redis.from_url('redis://localhost'), 100, 60)

Redis-token bucket i Lua

För att hantera toppar flyttar vi token bucket till ett Lua-skript. Tillståndet lagras i en Redis-hash med tokens och ts (senaste påfyllnadstid). Skriptet fyller på baserat på den förflutna tiden, försöker förbruka en token och skriver tillbaka tillståndet — allt atomiskt.

Returvärdet 1 betyder att begäran tillåts och 0 betyder att den begränsas. Vi anger också en TTL så att inaktiva klienter frigör sitt minne.

TOKEN_BUCKET_LUA = """
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local state = redis.call('HMGET', key, 'tokens', 'ts')
local tokens = tonumber(state[1]) or capacity
local ts = tonumber(state[2]) or now
local delta = math.max(0, now - ts)
tokens = math.min(capacity, tokens + delta * refill)
local allowed = 0
if tokens >= 1 then
  tokens = tokens - 1
  allowed = 1
end
redis.call('HMSET', key, 'tokens', tokens, 'ts', now)
redis.call('EXPIRE', key, math.ceil(capacity / refill) * 2)
return allowed
"""
# Invoked with EVAL via redis-py: script(keys=[key], args=[cap, refill, time.time()])

Ett återanvändbart FastAPI-beroende

Vi exponerar begränsaren som ett beroende, så att valfri route kan aktivera den. Beroendet härleder klientens identitet, kör begränsaren och kastar HTTPException(429) när kvoten är slut.

Föredra en autentiserad identitet (API-nyckel eller användar-ID) framför en rå IP-adress, eftersom IP-adresser delas bakom NAT och enkelt kan roteras av botar. Använd IP-adress som reserv endast för anonym trafik.

from fastapi import Request, HTTPException, Depends

def client_identity(request: Request) -> str:
    api_key = request.headers.get('x-api-key')
    if api_key:
        return f'key:{api_key}'
    return f'ip:{request.client.host}'

def rate_limit(limit: int, window: int):
    async def dependency(request: Request):
        ident = client_identity(request)
        limiter = request.app.state.limiter
        if not await limiter.allow(f'{ident}:{limit}:{window}'):
            raise HTTPException(status_code=429, detail='Rate limit exceeded')
    return dependency

# @app.get('/search', dependencies=[Depends(rate_limit(30, 60))])
# async def search(): ...

Ärliga 429-svar: Retry-After och headers

En korrekt begränsare är också en artig begränsare. Välfungerande klienter respekterar standardsignaler. Genom att skicka dem minskar ni antalet nya försök och supportärenden.

  • 429 Too Many Requests är den enda korrekta statusen. Använd aldrig 403 eller 503 för begränsning.
  • Retry-After anger hur många sekunder klienten ska vänta.
  • X-RateLimit-Limit, X-RateLimit-Remaining och X-RateLimit-Reset hjälper klienter att själva hålla rätt takt.

Låt även Lua-skriptet returnera det återstående antalet och återställningstiden, så att ni kan fylla i dessa headers utan extra round trips.

from fastapi import Request, HTTPException

async def enforce(request: Request, limit: int, window: int):
    limiter = request.app.state.limiter
    allowed, remaining, reset_in = await limiter.check(client_identity(request))
    if not allowed:
        raise HTTPException(
            status_code=429,
            detail='Rate limit exceeded',
            headers={
                'Retry-After': str(reset_in),
                'X-RateLimit-Limit': str(limit),
                'X-RateLimit-Remaining': '0',
                'X-RateLimit-Reset': str(reset_in),
            },
        )

Nivåindelade gränser och eskalering vid missbruk

En enda global gräns är trubbig. Produktionssystem kombinerar flera nivåer:

  • Gränser per route: en billig GET tål betydligt mer trafik än en kostsam sök- eller login-endpoint.
  • Nivåindelade identiteter: anonyma IP-adresser får en snäv kvot, autentiserade användare får mer och betalplaner mest.
  • Eskalering: när en klient upprepade gånger får 429 lägger ni till den i en Redis-blocklista med en exponentiell TTL, så att missbrukande botar får allt längre avstängningar.

Hjälpfunktionen nedan beräknar en fördubblad avstängningstid som begränsas till en timme.

def next_ban_seconds(strikes: int) -> int:
    base = 60  # 1 minute
    cap = 3600  # 1 hour
    return min(cap, base * (2 ** strikes))

for s in range(8):
    print(f'strike {s}: ban for {next_ban_seconds(s)}s')

Upptäck botar genom mer än bara räkning

Hastighetsbegränsning begränsar volymen, men avancerade botar håller sig strax under gränsen. Kombinera begränsning med billiga beteendesignaler:

  • Saknade eller skräpiga headers: en saknad User-Agent eller en UA som finns på en känd blocklista.
  • Andel misslyckade inloggningar: en hög kvot misslyckade i förhållande till lyckade autentiseringar per IP signalerar credential stuffing.
  • Sökvägsentropi: snabba träffar mot många orelaterade endpoints tyder på skrapning.
  • Proof of work / CAPTCHA som ett villkor när en poäng passerar en tröskel, i stället för en direkt blockering.

För in dessa signaler i en riskpoäng per klient i Redis och minska token bucket-kapaciteten dynamiskt för klienter med hög risk.

Snabbkontroll: Välj algoritm

Ert publika FastAPI-API körs över många Uvicorn-workers och flera pods. Legitima klienter skickar ibland korta legitima toppar, men ni måste upprätthålla ett stabilt långsiktigt genomsnitt och hålla beslutet konsekvent över alla instanser. Vilken design passar bäst?

Sammanfattning och checklista för produktion

Nu kan ni bygga distribuerad och missbrukstålig trafikbegränsning för FastAPI:

  • Centralisera tillståndet i Redis så att alla workers och pods delar en räknare.
  • Välj algoritm efter behov: fast tidsfönster för enkla gränser, glidande logg för exakthet och token bucket för att hantera toppar med ett upprätthållet genomsnitt.
  • Garantera atomicitet med Lua-skript via EVAL för att undvika race conditions vid läs-ändra-skriv.
  • Exponera begränsare som FastAPI-beroenden och basera dem i första hand på autentiserad identitet, med IP som reserv.
  • Svara ärligt med 429, Retry-After och X-RateLimit-*-headers.
  • Kombinera försvar: gränser per route och nivå, blocklistor med exponentiell backoff samt beteendebaserad botpoängsättning utöver ren räkning.

Öppna alltid kretsen försiktigt vid fel: om Redis inte kan nås ska ni medvetet avgöra om trafik ska tillåtas eller blockeras och larma om det.

Gratis att börja

Lär dig Bootcamp i backendutveckling med FastAPI med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
21
Lektioner
84

Vanliga frågor

Är lektionen ”Rate limiting och skydd mot botmissbruk” gratis?

Ja – du kan läsa vilka 3 lektioner som helst i lärvägen Bootcamp i backendutveckling med FastAPI, inklusive ”Rate limiting och skydd mot botmissbruk”, kostnadsfritt i sin helhet här på webben. Därefter låser CoddyKit PRO upp alla lektioner, plus interaktiv övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Kursen i Bootcamp i backendutveckling med FastAPI innehåller totalt 4 lektioner.

Vad lär jag mig i ”Rate limiting och skydd mot botmissbruk”?

Implementera distribuerad rate limiting och throttling med Redis för att hantera toppar och blockera missbrukande klienter. Ni övar på Bootcamp i backendutveckling med FastAPI med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig Bootcamp i backendutveckling med FastAPI?

Du behöver inga förkunskaper. Utbildningen i Bootcamp i backendutveckling med FastAPI på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 2 av 4.

Hur lång tid tar lektionen ”Rate limiting och skydd mot botmissbruk”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här Bootcamp i backendutveckling med FastAPI-lektionen?

Ja. Varje Bootcamp i backendutveckling med FastAPI-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Minska riskerna med OWASP API Security Top 10
  2. Rate limiting och skydd mot botmissbruk
  3. Hantering av hemligheter och nyckelrotation
  4. CORS, CSP och säkra headerprinciper
← Tillbaka till Bootcamp i backendutveckling med FastAPI