Rate limiting i praksis (det er ikke bare en tal-grænse)
At vælge rate limiting strategi minder lidt om at vælge klatresele: på afstand ligner de alle sammen hinanden, men den forkerte model føles først virkelig forkert, når du hænger og dingler halvvejs oppe på væggen. Det samme sker med APIer, bare med vrede brugere i stedet for højde.
I den her artikel går vi systematisk gennem tre klassiske modeller for rate limiting: fixed window, sliding window og token bucket. Ikke som teoretiske algoritmer på tavlen, men som tre konkrete måder at styre præcis det samme request-flow.
Hvad rate limiting faktisk skal redde dig fra
Rate limiting handler ikke om at straffe brugere. Det handler om at beskytte dit system, dine omkostninger og dine andre brugere mod dem, der larmer mest.
Jeg plejer at tænke på det i fire kategorier.
1. Abuse og bots
Alt det, du helst ikke vil have:
Masseskraping af dit indhold. Brute force login-forsøg. Nogen der har “glemt” at lave caching og bare hamrer dit API med 50 request i sekundet fra én klient.
Her skal rate limiting fungere som første forsvarslinje. Ikke perfekt sikkerhed, men nok til at det ikke kan misbruges helt uden friktion.
2. Spikes og uforudsete bursts
Du deployer en ny feature. Den bliver populær. Pludselig skyder loadet i vejret i 3 minutter, fordi nogen linker til det på forsiden et eller andet sted.
Hvis du ikke har en eller anden form for styring, så kan de tre minutter være nok til at slå alt andet offline. Eller til at din cloud-regning får et pænt lille hak opad.
3. Fairness mellem brugere
Forestil dig at du har 1.000 brugere og kapacitet til 1.000 requests i sekundet. Hvis én kunde får lov at bruge 900 af dem, står de andre i kø.
Rate limiting er også en fordelingsnøgle. Ikke bare per IP, men ofte per API-nøgle, per bruger, per konto eller per plan. Det er sådan noget, SaaS-udbydere typisk kobler til deres pricing.
4. Omkostninger og eksterne APIer
Nogle gange betaler du selv for noget i baggrunden: en tredjeparts API, database-læsninger, CPU-tung behandling.
Her kan rate limiting være forskellen på “vi havde en travl dag” og “vi sprængte budgettet i løbet af en time”. Særligt hvis nogen får fat i din token og bruger den uden at spørge pænt først.
Tre modeller, ét scenarie: sådan opfører de sig
Teori er fint, men det bliver først interessant, når vi binder det til konkrete tal.
Vi tager et tænkt API med denne regel:
Max 10 requests per minut per bruger.
Og så bruger vi det samme request-flow hele vejen:
00:00 - 10 requests på én gang (burst)
00:30 - 5 requests
01:10 - 5 requests
Vi antager, at vi starter med en tom limiter ved 00:00.
Fixed window rate limiting
Fixed window betyder: vi deler tiden op i faste intervaller, fx hele minutter fra 00:00 til 00:59, 01:00 til 01:59, og så videre. For hver bruger holder vi bare en counter per vindue.
Med vores scenarie:
Vindue 00:00 - 00:59, limit = 10
00:00 - 10 requests -> alle 10 tilladt (counter = 10)
00:30 - 5 requests -> alle 5 afvist (counter = 10, stadig samme minut)
Vindue 01:00 - 01:59, nyt limit = 10
01:10 - 5 requests -> alle 5 tilladt (counter = 5)
Det vil sige:
00:00: du får lov at lave et stort burst på 10.
00:30: du rammer muren, selvom du faktisk kun har lavet 10 requests på 30 sekunder.
01:10: alt er nulstillet, og du må igen.
Det er nemt at implementere, men det er ikke retfærdigt hen over glidende tid. Brugeren oplever det som “ugennemsigtigt”. Det føles tilfældigt, hvornår man rammer 429.
Sliding window rate limiting
Sliding window kigger på de seneste N sekunder i stedet for en kalenderagtig kasse. Vi siger: “Har du lavet mere end 10 requests det seneste minut fra nu?”.
Den simple version gemmer timestamps for requests og fjerner gamle. I praksis bruger man tit en mere komprimeret model, men mentalmodellen er:
Limit: max 10 requests de sidste 60 sekunder
00:00 - 10 requests -> alle 10 tilladt (historik: 10 events ved 00:00)
00:30 - 5 requests -> vi kigger tilbage til 23:30
der ligger 10 events inden for vinduet
alle 5 afvist
01:10 - 5 requests -> vi kigger tilbage til 00:10
de 10 events fra 00:00 er nu ældre end 60 sek
historik er tom
alle 5 tilladt
Bemærk forskellen fra fixed window: her handler det altid om “seneste minut”, ikke “kalenderminuttet”.
Hvis vi havde haft et andet flow, fx:
00:00 - 5 requests
00:30 - 5 requests
00:40 - 5 requests
Så ville fixed window tillade de første 10 og afvise alle 5 ved 00:40.
Sliding window ville:
00:00: 5 tilladt (5 i historik)
00:30: 5 tilladt (10 i historik)
00:40: kigge tilbage til 23:40. Der ligger 5 ved 00:00 og 5 ved 00:30, altså 10. Så de 5 ved 00:40 bliver afvist.
Brugeren oplever det mere konsistent: “Jeg må 10 per rullende minut”. Ingen magisk reset ved minutskifte.
Token bucket rate limiting
Token bucket har en lidt anden mental model. Tænk en spand, hvor der drypper tokens ned med en fast hastighed, fx 10 tokens per minut. Hver request bruger 1 token. Spanden har en maksimal størrelse, fx 10 tokens. Hvis spanden er tom, får du 429.
Med vores scenarie og 10 tokens max, 10 tokens per minut:
Start: bucket = 10 tokens
00:00 - 10 requests -> alle 10 bruger 10 tokens, bucket = 0
Mellem 00:00 og 00:30 fyldes spanden langsomt:
30 sekunder = 5 tokens
00:30 - 5 requests -> bucket = 5, så 5 tilladt, bucket = 0
Mellem 00:30 og 01:10 er der 40 sekunder -> ca. 6-7 tokens
Vi siger 6 tokens for simpelt regnestykke.
01:10 - 5 requests -> bucket = 6, så 5 tilladt, bucket = 1
Her ser du forskellen: token bucket tillader bursts, så længe du samlet set ikke bruger mere end den gennemsnitlige rate over tid. Du kan “spare op” til et burst, hvis du har været stille før.
Det er ofte det, man gerne vil have i praksis. Mennesker laver bursts: klikker lidt rundt, loader flere sider, holder pause.
Fixed window: hvornår den er fin, og hvornår den rammer forkert
Fixed window er den model, folk starter med, fordi den er nem.
Du har typisk noget i stil med:
key = user_id + ':' + current_minute_timestamp
INCR key
EXPIRE key efter 60 sekunder
Og så afviser du, hvis counteren går over din grænse.
Brug fixed window, hvis du kan leve med ujævnhed
Fixed window er ofte nok til:
Simple interne APIer, hvor det mest handler om at fange åbenlys misuse.
Admin-værktøjer eller backoffice, hvor få brugere er logget ind ad gangen.
Første version af en rate limiter, mens du stadig mest er i “vi skal bare have noget på”-fasen.
Den store fordel: implementeringstiden er kort. Du kan nærmest smide det ind over en eftermiddag, især med noget som Redis.
Problemet: wall-clock bias
Den klassiske fejl er, at man glemmer hvor hårdt vindueskanten kan ramme.
Forestil dig:
limit = 100 requests / minut
00:59 - 100 requests
01:00 - 100 requests
Med fixed window er det 200 requests på 2 sekunder, men du er teknisk set inden for limit.
Hvis du troede, at “100 per minut” betød noget i retning af jævnt fordelt load, så bliver du skuffet. Det er her mange opdager, at fixed window ikke nødvendigvis beskytter systemet mod peaks, men mest giver en løst optrukket grænse.
Sliding window: mere fair, lidt mere hjerne og CPU
Sliding window løser den skæve fordeling ved at kigge på rullende tid.
Den simple, naive version er:
Gem timestamp for hver request i en liste per bruger.
Når der kommer nyt request, fjern alle timestamps ældre end 60 sekunder.
Hvis der så stadig er ≥ limit, afvis.
Det kan fungere, men det skalerer dårligt, hvis der er mange events. Man ender typisk med at bruge varianter, der aggregerer i små buckets (fx per 10 sekunder) eller estimerer i stedet for at gemme alt.
Hvornår sliding window giver mening
Jeg ville kigge på sliding window, hvis:
Du vil være så fair som muligt over for rigtige brugere.
Du har reelle krav til at begrænse spikes, ikke kun langtidsgennemsnit.
Du har moderat load, hvor lidt ekstra kompleksitet i din limiter er ok.
Eksempel: login-endpoints, dyre rapport-kald, ting hvor hver request virkelig betyder noget. Her vil du gerne undgå, at to brugere med identisk adfærd får vidt forskellig behandling, bare fordi deres requests rammer forskellige side af et minutskifte.
Typisk fejl: sliding uden at optimere storage
Jeg har set flere løsninger, hvor nogen gemmer hver eneste request i en database-tabel for at implementere sliding window. Det fungerer til dev, det brænder sammen i produktion.
Hvis du går sliding-vejen, så overvej noget som Redis med små tidsbuckets, eller brug færdige mønstre som dem, der er beskrevet i Redis’ egen rate limiting dokumentation.
Token bucket: burst-venlig og et godt default
Token bucket ender ofte som et godt kompromis: du begrænser gennemsnitsraten over tid, men du straffer ikke korte, legitime bursts unødigt.
Den basale model kræver, at du for hver bruger gemmer:
current_tokens
last_refill_timestamp
Hver gang der kommer et request:
1) Udregn hvor mange tokens der burde være tilføjet siden sidst.
2) Opdater current_tokens, men aldrig over max.
3) Hvis current_tokens > 0, træk 1 og tillad.
4) Ellers: afvis med 429.
Eksempel: simpel token bucket i pseudokode
config:
rate_per_sec = 1 # 60 i minuttet
bucket_size = 10
state per user:
tokens
last_refill_ts
function allow_request(user_id, now):
state = load_state(user_id)
elapsed = now - state.last_refill_ts
refill = elapsed * rate_per_sec
state.tokens = min(bucket_size, state.tokens + refill)
state.last_refill_ts = now
if state.tokens >= 1:
state.tokens -= 1
save_state(user_id, state)
return true
else:
save_state(user_id, state)
return false
Du kan optimere og justere, men strukturen er den samme.
Hvornår token bucket er et godt valg
Jeg ville typisk vælge token bucket som udgangspunkt, hvis:
Du har APIer med menneskelige brugere bagved, der naturligt laver bursts.
Du vil undgå “det føles tilfældigt”-klager, uden at implementere tung sliding window logik.
Du har behov for at kunne “spare op” til kortere spikes uden at skade resten af systemet.
Fx et public read-API, hvor de fleste laver et par klik hist og her, men enkelte kunder har dashboards, der laver 10-20 requests i et hug. Token bucket gør typisk begge typer glade.
429, Retry-After og gode rate limit headers
En rate limiter er ikke færdig, bare fordi du kan sige “true” eller “false”. Klienterne skal kunne reagere fornuftigt, når du siger nej.
Statuskoden er ret klar: 429 Too Many Requests. Den er defineret i RFC 6585, og MDN beskriver den også fint.
Retry-After: sig hvornår de må prøve igen
Retry-After headeren fortæller klienten, hvor lang tid der går, før det giver mening at prøve igen. Du kan sende den som antal sekunder eller en dato.
Eksempel i et 429-svar:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json
{
"error": "rate_limited",
"message": "Too many requests, try again in 30 seconds"
}
Med token bucket kan du ofte beregne en ret præcis værdi: hvor lang tid går der, før der er mindst 1 token igen.
Rate limit headers: giv klienten et speedometer
Det er en god idé at afsløre lidt af limit-status i headers, så klienter (og udviklere) kan se, hvor de ligger.
Et udbredt mønster er:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 12
X-RateLimit-Reset: 1713950400
Hvor:
Limit er den maksimale mængde i dit vindue/bucket.
Remaining er hvor mange requests der er tilbage lige nu.
Reset er et timestamp for hvornår tælleren resetter eller når bucket er fyldt op igen.
Du kan også bruge standardiserede header-navne afhængigt af platform, men mønstret er det samme.
Hvor skal din rate limiter bo? In-memory, Redis eller edge
Selve algoritmen er kun halvdelen af arbejdet. Den anden halvdel er: hvor lægger du state, så det både er hurtigt og korrekt på tværs af instanser.
In-memory: hurtigt, men kun til små setups
Hvis du kører én eneste instans af din backend, kan du gemme limiter-state i hukommelse. Det er hurtigt, simpelt, og du slipper for en ekstra komponent.
Problemet kommer, når du skalerer horisontalt:
Bruger A rammer instans 1 og får grønt lys.
Næste request rammer instans 2, der tror, at brugeren er helt frisk.
Resultat: du har mange små lokale limiters, der ikke taler sammen. Det kan være fint til “blød” beskyttelse, men ikke hvis du har hårde krav.
Redis: det klassiske valg til distribueret rate limiting
Redis er et godt kompromis:
Hurtig in-memory datastore.
Deles mellem alle dine instanser.
Understøtter atomare operationer, som du kan bruge til at undgå race conditions.
Typisk mønster:
Key per bruger (eller per API-nøgle).
Lua-scripts til at implementere token bucket eller sliding window logik atomart.
Hvis du hoster på fx en platform med serverless functions eller edge, så kig også på deres egne kvote-systemer, men Redis er stadig en god generel løsning.
Edge og CDN: rate limiting tæt på brugerne
Nogle udbydere (Cloudflare, Fastly, etc.) tilbyder rate limiting direkte på kanten, før trafikken rammer din origin-server.
Fordele:
Du beskytter din infrastruktur tidligere i flowet.
Du kan smide åbenlys misbrug væk, før det når dit API.
Ulemper:
Det er ofte konfigureret via UI/regler og ikke så fleksibelt som egen kode.
Det kan være sværere at koble direkte til dine forretningsregler, fx forskellige limits per prisplan.
En model, jeg godt kan lide, er:
Brug CDN/edge rate limiting til basic per-IP begrænsning.
Brug egen Redis-baseret limiter til per-bruger/per-plan limits inde i APIet.
Sådan vælger du model uden at ødelægge brugeroplevelsen
Til sidst: hvordan sætter du det hele sammen, så du ikke får en support-indbakke fuld af “dit API er nede” mails, bare fordi folk rammer 429 på uforudsigelige tidspunkter.
1. Start med din mental model, ikke algoritmen
Stil dig selv spørgmålet: “Hvad betyder 100 requests per minut for mig?”.
Vil du begrænse:
Hard spikes? Så overvej sliding window eller en stram token bucket.
Langtidsgennemsnit, men tillade korte bursts? Så er token bucket et godt bud.
Bare det værste misbrug med minimal indsats? Fixed window kan være nok.
2. Design dine 429-svar som en del af APIet
429 er ikke en fejl “du håber aldrig sker”. Den er en del af kontrakten.
Gør det her:
Brug 429 konsekvent til rate limiting.
Send Retry-After, ikke bare et vagt JSON-svar.
Eksponer limit og remaining i headers, så klienter kan tilpasse sig.
Dokumentér det tydeligt i din API-dokumentation.
Hvis du arbejder med frontend også, så beslut: viser vi en besked, backoff-er vi automatisk, eller begge dele?
3. Giv forskellige limits til forskellige handlinger
Ikke alle endpoints er lige dyre.
Du kan sagtens have:
Højere limits på billige read-endpoints.
Lavere limits på dyre write- eller rapport-endpoints.
Separate limiters for fx login-forsøg og data-API.
Det kræver lidt mere konfiguration, men sparer dig både load og irriterede brugere, der bliver blokeret på “alt”, fordi de spammer én tung operation.
4. Overvåg hvordan din limiter opfører sig
Selvom det føles fristende, så sæt ikke bare tal og glem dem.
Log i det mindste:
Antal 429 per endpoint.
Antal brugere der rammer limit dagligt.
Mønstre: kommer 429 i bursts, eller som jævn støj?
Hvis du ser mange legitime brugere ramme muren, er det måske ikke brugerne, men dine tal, der er for stramme. Eller din model, der ikke matcher den adfærd, du faktisk ser.
5. Byg limiteren som et separat lag
Et sidste råd: prøv at holde din rate limiting logik adskilt fra din forretningslogik.
Fx:
En middleware, der checker rate limit og afgør “tillad” / “afvis med 429”.
Din faktiske handler antager, at hvis den kører, så er requestet godkendt ift. limit.
Det gør det langt nemmere at skifte fra fx fixed window til token bucket senere uden at skulle skrive hele dit API om. Og hvis du er nørdet nok til at læse helt herned, er chancen pænt stor for, at du på et tidspunkt får lyst til at tweake det.
Og ja, din første version bliver sikkert enten for stram eller for løs. Det er helt normalt. Det svarer lidt til at sætte første sikkerhedsgrip lidt for højt på klatrevæggen. Du finder balancen, når du har hængt der et par gange.









Send kommentar
Du skal være logget ind for at skrive en kommentar.