Du smadrer dit API hvis alle må hamre løs uden grænser

Du smadrer dit API hvis alle må hamre løs uden grænser

Scenariet hvor du opdager for sent at du mangler rate limiting

Mit mål her er simpelt: et API der ikke vælter, selv når nogen (bevidst eller ubevidst) holder F5 nede eller har skrevet en alt for ivrig klient.

Forestil dig, at du har et lille API til en side med brugere og ordrer. Alt kører fint. Indtil:

  • En mobil-app får en bug og sender 20 requests i sekundet fra hver bruger.
  • Nogen laver et script, der prøver tusind passwords af i minuttet.
  • En ekstern integration glemmer caching og kalder dit dyreste endpoint konstant.

Resultatet er det samme: CPU spidser, databasen sveder, rigtige brugere får timeouts, og du sidder og bander over logs en sen aften. Det her er stedet, hvor rate limiting burde have taget stød først.

Før vs nu – et API uden og med rate limiting

For at holde styr på tingene bruger jeg en simpel før/nu-opsætning. Før: alt er åbent. Nu: du sætter fornuftige grænser.

Uden rate limiting Med rate limiting
En fejl i klienten kan sende hundredevis af requests i sekundet Klienten bliver bremset automatisk efter X requests per minut
Brute force-angreb kan prøve tusindvis af logins hurtigt Efter få forsøg bliver det langsomt eller blokeret midlertidigt
Ressourcekrævende endpoints kan overbelaste databasen Dyre endpoints får strammere grænser end de billige
Du opdager misbrug når alt allerede er langsomt eller nede Du ser 429 i logs og kan justere grænserne løbende
Klienter gætter selv på hvad der er “for meget” Klienter får 429 + Retry-After og kan tilpasse sig

Pointen: Du kan ikke forhindre al misbrug, men du kan styre tempoet. Og det er faktisk ofte nok.

Hvad er rate limiting i et API, helt jordnært?

Hvis du søger på rate limiting API, får du hurtigt en masse smarte ord. Jeg plejer at koge det ned til:

Rate limiting er bare: “Hvor mange gange må denne nøgle gøre denne ting i dette tidsrum?”

Nøgle kan være:

  • IP-adresse
  • Bruger-id
  • API-nøgle
  • Kombinationer, f.eks. bruger + endpoint

Og “denne ting” er typisk et endpoint eller en gruppe af endpoints.

Eksempel:

// Fritekst-formulering
"Hver bruger må kalde /api/orders max 60 gange per minut"

Det er faktisk en hel rate limit-regel der. Resten er implementation og små detaljer.

Hvad klienten ser – 429 og en ordentlig besked

Hvis du laver rate limiting, der føles som en mur, folk bare løber ind i, får du vrede brugere og grimme support-mails.

HTTP har faktisk en statuskode til formålet: 429 Too Many Requests.

En god 429-respons indeholder typisk:

  • Status: 429
  • Header: Retry-After (sekunder eller dato)
  • Evt. headers: hvor meget der er tilbage i vinduet
  • Body: klar fejlbesked i et fast format

Et simpelt 429-svar som er til at arbejde med

HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1713871200
Content-Type: application/json

{
  "error": "too_many_requests",
  "message": "Du har kaldt dette endpoint for ofte. Prøv igen om 30 sekunder.",
  "retry_after_seconds": 30
}

Som klient-udvikler kan du nu:

  • Vent pænt i 30 sekunder (eller backoff med lidt ekstra margin).
  • Vise en forståelig fejl til brugeren.
  • Logge at du rammer limit for tit og optimere.

På MDN kan du læse mere om HTTP 429, hvis du vil nørde alle detaljer.

Tre basale strategier – fast vindue, glidende vindue, buckets

Nu kommer den klassiske sammenligning: gammel, kantet løsning vs lidt mere sofistikeret.

Fixed window – den klassiske, lidt skæve løsning

Idé: Du tæller hvor mange requests der kommer i f.eks. hvert minut. Hver gang uret runder et minut, nulstiller du.

// Eksempelregel
Max 60 requests per IP per minut på /api/search

Fordele:

  • Nem at tænke: “60 per minut”.
  • Nemt at implementere med f.eks. Redis-key per minut.

Ulemper:

  • Brugere kan “burst”: 60 requests de sidste sekunder af minut 1, og 60 mere de første sekunder af minut 2.
  • Ikke super fair ift. dem der spreder deres trafik jævnt.

Sliding window – lidt mere fair

Idé: I stedet for at kigge på faste minutter, kigger du altid tilbage i de sidste X sekunder.

Eksempel: “Max 60 calls i de sidste 60 sekunder”.

Det fjerner grimme kanter, men er lidt tungere at implementere, fordi du skal gemme tidsstempler eller beregne pænt i f.eks. Redis.

Token bucket vs leaky bucket – styr på bursts

Her er vi over i de modeller, du ser nævnt i dokumentation til f.eks. Redis rate limiting-patterns.

Token bucket (god til bursts)

Idé: Forestil dig en spand med tokens. Hver gang en request kommer, bruger den et token. Tokens fyldes langsomt op igen med en fast hastighed.

  • Du kan tillade korte bursts, hvis spanden har nået at blive fyldt.
  • Over tid må gennemsnittet ikke overskride en bestemt rate.

Leaky bucket (mere stabil strøm)

Idé: Requests lægges i en kø som i en spand med et lille hul i bunden. De siver ud med en fast hastighed. Hvis der hældes for meget på, løber det over, og de ekstra bliver droppet.

  • God til at udjævne trafik.
  • Mindre bursts oven i hinanden.

Hvilken strategi skal du starte med?

Hvis du bygger et mindre API, er mit forslag:

  • Start med fixed window eller en simpel token bucket.
  • Brug en delt storage (Redis, database) så flere instanser kan dele state.
  • Skift først til sliding window eller mere avanceret, når du har et konkret problem.

Det vigtige er, at du har en grænse, ikke at den er matematisk perfekt.

Hvad skal nøglen være – IP, bruger eller API key?

Her bliver tingene hurtigt lidt politiske: Hvem prøver du at være fair overfor?

Per IP-adresse – simpelt, men ikke altid fair

Godt til: anonyme endpoints, offentlige API’er uden login, basic beskyttelse mod bots.

Problemer:

  • Mange brugere bag samme IP (kontor, skole, mobilnet) kan ramme grænsen sammen.
  • Angribere kan skifte IP (VPN, botnet), især ved seriøse angreb.

Per bruger-id – bedre fairness for loggede brugere

Godt til: API’er hvor brugeren logger ind.

Du beskytter hver bruger individuelt. Én ivrig bruger spærrer ikke de andre inde.

Problemer:

  • Kræver at du kender brugeren tidligt i request-flowet.
  • Login-endpointet kan ikke bruge bruger-id, for du kender det ikke endnu.

Per API-nøgle – standard i tredjeparts-integrationer

Godt til: B2B-integrationer, hvor hver kunde har sin egen nøgle.

Du kan give forskellige kunder forskellige limits, f.eks. efter prisplan.

Kombinationer du faktisk får brug for

I praksis ender jeg tit med en kombination:

  • Login-endpoint: rate limit per IP + per bruger-navn (for at bekæmpe brute force, som også OWASP foreslår i deres kontrol mod brute force).
  • Øvrige endpoints: per bruger-id eller per API-nøgle.
  • Særligt tunge endpoints: ekstra strikt grænse oveni.

Hvis du er i tvivl, så start per bruger eller per API-nøgle, og læg IP-limits ovenpå på udvalgte steder, f.eks. login.

Hvordan vælger du selve grænserne uden at skyde dig i foden?

Her er det nemt at blive for forsigtig eller alt for gavmild.

Jeg plejer at tænke i tre niveauer:

  • Basis: almindelige endpoints, som kan caches og ikke er dyre.
  • Dyre: søgninger, store aggregeringer, rapporter.
  • Sikkerhedsfølsomme: login, password reset, e-mail-verificering.

Forslag til start-værdier

Det her er ikke facit, men noget du kan sætte i din kode i dag og justere fra:

  • Login: 5 forsøg per minut per IP + 20 per time per IP + 10 per time per brugernavn.
  • Almindelige GET-endpoints: 120 requests per minut per bruger.
  • Søgning / dyre queries: 30 requests per minut per bruger.
  • Upload / skrivende endpoints: 60 requests per minut per bruger.

Og ja, det ser tilfældigt ud. Det er det også. Pointen er, at du har en barriere, du kan skrue op eller ned.

Justering over tid

Efter du har deployet, så kig i logs 1-2 uger:

  • Hvor ofte rammer folk 429?
  • Er det de samme brugere / IP’er?
  • Hvilke endpoints har flest begrænsninger?

Hvis rigtige brugere ofte rammer grænsen under normal brug, har du sat den for lavt eller skal undtage nogle endpoints (f.eks. små metadata-kald) fra de hårde regler.

Hvis du aldrig ser en 429, er grænsen måske så høj, at den i praksis ikke gør noget. Det er også en slags info.

Implementering i Node – et lille start-eksempel

Nu til noget kode. Her er et simpelt Node/Express-eksempel med en fixed window-rate limit. Ikke produktion-perfekt, men godt nok til at forstå mønsteret.

En meget simpel in-memory rate limiter

Det her virker kun på én server og nulstilles ved restart, men er fint til at lege med lokalt.

const express = require('express');
const app = express();

// key: ip, value: { count, windowStart }
const requests = new Map();

const WINDOW_MS = 60 * 1000; // 1 minut
const MAX_REQUESTS = 60;     // 60 requests per minut

function rateLimit(req, res, next) {
  const key = req.ip;
  const now = Date.now();

  let entry = requests.get(key);

  if (!entry || now - entry.windowStart > WINDOW_MS) {
    entry = { count: 0, windowStart: now };
  }

  entry.count += 1;
  requests.set(key, entry);

  const remaining = Math.max(0, MAX_REQUESTS - entry.count);
  const resetInMs = WINDOW_MS - (now - entry.windowStart);

  res.setHeader('X-RateLimit-Limit', MAX_REQUESTS);
  res.setHeader('X-RateLimit-Remaining', remaining);
  res.setHeader('X-RateLimit-Reset', Math.floor((now + resetInMs) / 1000));

  if (entry.count > MAX_REQUESTS) {
    const retryAfterSeconds = Math.ceil(resetInMs / 1000);
    res.setHeader('Retry-After', retryAfterSeconds);

    return res.status(429).json({
      error: 'too_many_requests',
      message: `For mange requests. Prøv igen om ${retryAfterSeconds} sekunder.`,
      retry_after_seconds: retryAfterSeconds,
    });
  }

  next();
}

app.use('/api/', rateLimit);

app.get('/api/hello', (req, res) => {
  res.json({ message: 'Hej, verden' });
});

app.listen(3000, () => {
  console.log('Server kører på http://localhost:3000');
});

Når du har leget med den her, kan du skifte til en Redis-baseret løsning, så du kan køre flere instanser af din app uden at snyde limitten.

Hvis du vil se mere om generel API-design, så har jeg skrevet om det i en tidligere artikel om forudsigelige API’er.

App vs gateway/CDN – hvor skal rate limiting bo?

Du har typisk to mulige steder at placere rate limiting:

  • Direkte i din app-kode.
  • Foran appen: reverse proxy, API-gateway eller CDN (Cloudflare, NGINX, Kong osv.).

Rate limiting i app-koden

Fordele:

  • Nemt at have forskellig logik per endpoint.
  • Lettere at lave bedre fejl-bodies, f.eks. dit eget JSON-fejlformat.
  • Du kan bruge samme validering/fejlstruktur som resten af API’et.

Ulemper:

  • Din app bliver stadig ramt, før du blokerer.
  • Kræver delt storage for at virke på tværs af instanser.

Rate limiting i gateway eller CDN

Fordele:

  • Trafikken bliver stoppet længere ude, før den rammer din app og database.
  • Ofte ret nemt at slå til med konfiguration.
  • Nemt at skalere, fordi gatewayen står for state.

Ulemper:

  • Svære custom-fejlformater (nogle gateways kan dog godt).
  • Mindre fleksibel logik per endpoint, medmindre du virkelig konfigurerer igennem.

Min normale løsning i små projekter:

  • Brug gateway/CDN til de grove, IP-baserede limits.
  • Brug app-kode til de mere fine per-bruger/per-nøgle-limits og pæne fejlbeskeder.

Test din rate limiting uden at skyde rigtige brugere ned

Når du har limits på plads, er næste skridt at teste dem. Ikke kun at de blokerer, men at de ikke blokerer alt.

1. Reproducer 429 bevidst

Brug f.eks. curl eller et lille Node-script til at sende mange requests hurtigt.

for i in {1..80}; do
  curl -s -o /dev/null -w "%{http_code}n" 
    http://localhost:3000/api/hello
done

Tjek at du faktisk begynder at få 429 efter det forventede antal.

2. Tjek headers og fejlformat

Se på rå responsen:

curl -i http://localhost:3000/api/hello

Tjek at dine X-RateLimit-*-headers og Retry-After giver mening, og at JSON-fejlen ser pæn ud.

3. Kør realistiske flows

Lav en lille script eller brug noget som kører gennem “normal brug”:

  • Login.
  • Hent profil.
  • Hent liste.
  • Søg lidt.

Kør det 10-20 gange i træk. Du skal ikke ramme 429 ved normal adfærd. Hvis du gør, er dine limits for stramme.

4. Overvåg og log

Log alle 429-responses, gerne med:

  • Hvilken nøgle (IP / bruger / API-nøgle).
  • Hvilket endpoint.
  • Hvor meget der manglede for at være indenfor grænsen.

På den måde kan du senere justere de endpoints der enten er over- eller under-beskyttet.

En lille start-opskrift du kan kopiere direkte

Hvis du skal i gang i et mindre projekt med et Node-API, vil jeg gøre sådan her:

  1. Vælg nøgler:
    • Login: per IP + per brugernavn.
    • Andre endpoints: per bruger-id eller per API-nøgle.
  2. Vælg strategi:
    • Start med fixed window i én Redis-key per nøgle+endpoint.
  3. Vælg output:
    • Brug 429 Too Many Requests.
    • Sæt Retry-After og simple X-RateLimit-*-headers.
    • Returnér JSON med error, message og retry_after_seconds.
  4. Vælg startgrænser:
    • Login: 5/min per IP, 20/time per IP, 10/time per brugernavn.
    • GET: 120/min per bruger.
    • Dyre endpoints: 30/min per bruger.
  5. Log 429:
    • Gem nøgle, endpoint og tidspunkt.
    • Lav evt. et lille admin-endpoint der viser statistik.

Så har du faktisk en hel rate limiting-løsning, du kan bygge videre på, uden at den føles som raketvidenskab.

Hvis du vil arbejde mere med backend-grundbegreber, kan du også kigge på artiklen om SQL-indeks, som tit er det næste flaskehals-problem efter manglende rate limiting.

Til sidst – det farligste er ikke angrebene, men høfligheden

De fleste små API’er går faktisk ikke ned på grund af superavancerede angreb.

De går ned på grund af høflige klienter uden grænser, der gør præcis hvad du sagde, men alt for hurtigt.

Hvis du ikke sætter tempoet for dit API, skal du ikke blive overrasket, når nogen træder speederen i bund.

Fixed window er nem at implementere og fint til simple scenarier, men giver burst-problemer ved vinduesskift. Sliding window eller sliding log er mere præcise men kræver mere lagring. Token bucket er ofte et godt kompromis: den tillader kontrollerede bursts samtidig med at den håndhæver en gennemsnitshastighed.
Brug en delt, hurtig datastore som Redis til atomiske operationer (INCR+EXPIRE) eller Lua-scripts for konsistens, eller flyt logikken til din API-gateway/proxy (f.eks. NGINX, Kong, Cloudflare). Undgå kun-local in-memory grænser medmindre du har en fallback-synkronisering; sørg også for at håndtere clock-drift og netværksfejl.
Start med at måle: p95-latency, databaseniveau og nuværende trafikmønstre. Sæt strengere grænser for dyre endpoints, differentier efter brugerplan eller nøgle, og deploy gradvist med overvågning af 429-rater før du justerer. Brug feature flags eller konfigurationslag så du kan tune grænser uden deploy.
Respekter Retry-After når den findes, og implementer eksponentiel backoff med jitter for retries for at undgå synkrone retry-bølger. Hvis en klient får gentagne 429s, øg backoff-tiden eller degradér funktionalitet (f.eks. cache svar lokalt) i stedet for at forsøge konstant genkald.

Jonas Kirkeby har skrevet kode siden han som teenager forsøgte at lave en helt simpel hjemmeside til sin fars lille vvs-firma – og endte med at sidde oppe hele natten for at få en knap til at skifte farve. Siden da har han lært sig det meste ved at prøve sig frem, kopiere andres eksempler, ødelægge dem og langsomt forstå, hvorfor tingene virker, som de gør.

Til daglig arbejder han slet ikke med IT, men bruger aftener og morgener på små projekter: en lille side til en forening, et simpelt værktøj til at holde styr på familiens madplan eller et Python-script, der rydder op i rodede filer. Det er den slags konkrete hverdags-behov, der har formet hans måde at tænke kodning på – hvad kan jeg bygge nu, som faktisk hjælper mig eller nogen, jeg kender?

På Coding Class deler Jonas de guides, han selv ville ønske, han havde haft: korte, konkrete forløb, hvor du kan se noget på skærmen efter få minutters læsning. Han viser hele vejen fra idé til færdig løsning, inklusive de typiske fejl og små snubletråde på vejen, så du ikke kun får den pæne, polerede version.

Hans mål er, at du som begynder eller let øvet hurtigt får følelsen af: “Det her kan jeg faktisk selv finde ud af” – uanset om du vil bygge din første lille hjemmeside, forstå JavaScript-funktioner eller bruge Python til at automatisere en kedelig opgave.

Send kommentar

You May Have Missed