Rate limiting i praksis (det er ikke bare en tal-grænse)

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.

Brug en central, atomisk datastore som Redis og udfør tælling eller token-uddeling via INCR+EXPIRE eller Lua-scripts for at sikre atomicitet. For token bucket og sliding-window er Redis sorted sets eller scripts almindelige mønstre; som alternativ kan du bruge en managed API-gateway der håndterer konsistens for dig. Undgå at stole på lokale clocks - brug server-side monotone tidsstempler eller synchronized clock services.
Returner 429 ved afvisning og inkluder informative headers som RateLimit-Limit, RateLimit-Remaining og RateLimit-Reset eller standardiserede varianter. Brug også Retry-After for at angive hvornår klienten kan prøve igen, og send en kort JSON-fejltekst med forslag til backoff eller kontaktinfo for undtagelser.
Start med at måle dit nuværende trafikmønster og kapacitet - brug p95/p99 for at sætte bæredygtige grænser, ikke kun gennemsnittet. Differentier efter konto/type (anon, auth, betalt), tillad en lille burst-buffer, og juster iterativt ud fra metrics som afvisningsrate, latency og omkostninger.
Brug token bucket eller leaky-bucket for at tillade korte bursts mens du begrænser langsigtet throughput. Kombinér det med prioritering (fx kø for baggrundsjob eller højere burst for betalende kunder) og blid degradering som kø eller rate-limited svar i stedet for hårde afvisninger.

Sara Vestergaard er selvlært kode-nørd, der stille og roligt er gået fra at rode med en enkelt HTML-side til at bygge små værktøjer, scripts og hjemmesider til sig selv og vennerne. Hun startede med at lave en simpel band-hjemmeside som teenager og opdagede, hvor tilfredsstillende det er, når noget, du har skrevet, pludselig lever på skærmen.

For Sara handler kodning ikke om store ord eller imponerende titler, men om meget konkrete problemer: den kedelige opgave, der tager for lang tid, den ven der mangler en lille porteføljeside, eller den liste, der burde sortere sig selv. Hun elsker at pille ting fra hinanden – også kode – for at se, hvad der egentlig foregår, og hun har brugt utallige aftener på at google fejlbeskeder, teste små eksempler og langsomt bygge sin forståelse op.

På Coding Class deler hun den tilgang videre. Hun skriver til dig, der gerne vil lære at kode ved at gøre det i praksis: små projekter, korte kodebidder og forklaringer, der hænger sammen med det, du faktisk sidder med på skærmen. Hun skærer ind til benet, viser typiske fejl og deres løsninger og giver altid et forslag til, hvordan du kan bygge en tand videre, når grundideen først virker.

Når hun ikke skriver til Coding Class eller nørkler med nye små projekter, hænger Sara på klatrevæggen, vander sine altanplanter eller spiller gamle Nintendo-spil. Men hun ender næsten altid tilbage ved tasterne – for der er altid endnu en lille ting, der kunne være smartere, hurtigere eller bare lidt sjovere at bruge.

Send kommentar

You May Have Missed