Kontext
Die KI-Endpunkte des Portfolios (/api/chat, /api/nl-sql) sprechen mit
Anthropic. Jeder Aufruf kostet Geld. Drei Schutzgitter wollte ich vor dem
öffentlichen Start des Chats:
- Ein globales Tagesbudget in Cent — deckelt die Gesamtausgaben.
- Eine IP-bezogene Tagesgrenze — begrenzt den Schaden eines einzelnen Angreifers.
- Request-Vorprüfungen — Bot-UA-Filter, Origin-Wächter,
Body-Größenlimit,
Content-Length-Pflicht — um den billigen Junk vor dem Modell abzuweisen.
Die einfache Implementierung lebt in src/lib/rate-limit.ts: zwei
Map<string, ...>-Objekte mit Reservation + Commit, damit die tatsächlichen
Modellkosten — erst nach dem Streaming bekannt — gegen die zur
Request-Eingangszeit genommene Reservation abgeglichen werden.
Das Audit (27.05.2026) hat es korrekt gekennzeichnet: der In-Memory-Store überlebt Vercel-Funktionsinstanzen nicht. Zwei Requests, die parallel auf zwei warme Lambdas treffen, können beide bis zum Maximum reservieren.
Ich habe ihn trotzdem ausgeliefert.
Was ich vor der Entscheidung wissen wollte
Für ein Upgrade auf einen verteilten Store (Upstash Redis ist der naheliegende Fit auf Vercel) brauchte ich:
- Die tatsächliche Traffic-Form. Eine Portfolio-Seite hat im Mittel ~0 gleichzeitige KI-Requests. Das relevante Szenario ist nicht der Normalbetrieb, sondern „was passiert, wenn diese Seite auf der HN-Startseite landet". Reale Failure-Mode, niedrige Jahresfrequenz.
- Die tatsächliche Kostenobergrenze. Das Tagesbudget liegt bei 1/Tag über parallele warme Lambdas. Auf Vercel hält ein Portfolio selten mehr als 1–2 gleichzeitig warm. Plausibler Worst-Case-Overshoot: $2–3 an einem viralen Tag. Als Einmalereignis akzeptabel; als Dauerzustand nicht.
- Die Migrations-Form. Wie sieht der Code mit Upstash aus? Wie viel von der aktuellen API-Oberfläche ändert sich?
Was ich gefunden habe
Traffic — noch kein Problem
Realistische Last: eine Handvoll Recruiter-Besuche pro Woche, ein paar Chat-Austausche pro Besuch. Multi-Instance-Fanout ist ein Nicht-Problem, weil Multi-Instance gar nicht aktiviert wird. Das im Audit markierte Bypass-Szenario braucht parallele Traffic-Lasten auf mehreren warmen Lambdas — passiert hier organisch nicht.
Kosten — durch das Tagesmaximum begrenzt
Das Tagesmaximum ist der eigentliche Sicherungsschalter. Der im Audit
markierte Overshoot ist nach oben durch N × cap_per_day begrenzt. Setze
cap_per_day auf ein Budget, das ich an einem viralen Tag absorbieren
würde, und der Overshoot ist per Definition tolerabel. Wird diese Seite
in Zukunft kostensensitiv, ist das Cap der Hebel, nicht der Store.
Migrations-Form — jetzt skizziert, nicht jetzt ausgeliefert
Die Kosten, das auf „später" zu schieben, sind die Kosten, die verteilte
Arbeit unter Druck zu machen, wenn der Traffic tatsächlich kommt. Um den
Druck abzufedern, habe ich ein RateLimitStore-Interface und eine Skizze
des Upstash-REST-API-Adapters entworfen (keine SDK-Abhängigkeit nötig —
Upstash bietet reines HTTP):
// src/lib/rate-limit-redis.ts (Skizze)
export interface RateLimitStore {
reserve(key: string, cents: number): Promise<ReserveResult>;
commit(key: string, actualCents: number): Promise<void>;
}
Der aktuelle In-Memory-Store würde dasselbe Interface adoptieren. Routes
wechseln vom synchronen reserveBudget(3) auf await reserveBudget(3)
hinter einer Feature-Flag-ENV-Variable; bei Bedarf umlegen; der Rest des
Codes bleibt unverändert.
Was ich entschieden habe
Den In-Memory-Store behalten. Das Interface ausliefern. Den Migrations-Trigger dokumentieren.
Trigger zum Wechsel auf Upstash:
- Anhaltender paralleler Traffic. Zwei oder mehr warme Instanzen, die gleichzeitig KI-Requests bedienen, häufiger als nicht.
- Ein virales Ereignis. Ein Spike, bei dem
Tagesmaximum × Instanzanzahlim Kontoauszug schmerzt, nicht nur auf dem Papier. - Eine zweite Portfolio-Oberfläche. Wenn eine künftige Fallstudie einen dritten KI-Endpunkt hinzufügt und das gemeinsame Budget routen- übergreifend wird, bricht die per-Store-pro-Route-Abrechnung zusammen.
Bis dahin gewinnt der einfachere Code-Pfad:
- Kein externer Dienst im Request-Pfad → weniger Failure-Modes, keine Upstash-Latenz auf dem Kaltpfad.
- Kein Async-Refactor durch drei Route-Handler und die Test-Suite.
- Die Kostenobergrenze ist das Tagesmaximum, das auf einer Zahl steht, die ich auch im Worst-Case-Overshoot absorbieren würde.
Der Boring-Tech-Move ist, den In-Memory-Store zu behalten und diese Entscheidung aufzuschreiben, damit der nächste Wartende (wahrscheinlich ich, in einem Jahr) nicht alles neu herleiten muss.
Der Trigger, auf den ich achte
Eine einzige Zeile in den Vercel-Function-Logs:
event:"daily_budget_exhausted", mehr als einmal pro Woche. Das ist das
strukturierte Event, das das neue src/lib/observability.ts emittiert,
wenn eine Reservation abgelehnt wird, weil der globale Counter das Cap
erreicht hat. Sobald das wiederkehrend feuert, ist es kein Worst-Case
mehr, sondern der Medianfall, und der per-Instance-Overshoot hört auf,
ein Rundungsfehler zu sein.