Strategy
LLM-audit voor SaaS-ops: zes regels die outages vangen
Nadat de rate-limit cliff een klant midden in een sprint raakte, schreven we de zes-regelaudit op die we nu draaien op elke Nederlandse SaaS-stack onder de €25M.

Dinsdag, 14:40 Amsterdamse tijd. De customer-success agent stopt midden in een antwoord aan een betalende klant. Slack ontploft. De Bedrock-console staat groen, maar het model achter de schermen geeft 429s terug op drie van de vier productieroutes. De ops-lead pingt de on-call backend engineer. Die zit in een camper ergens ten noorden van Bergen in Noorwegen, geen bereik tot vrijdag.
Dit is het moment waarop een vendor-chain audit zichzelf terugbetaalt, of niet bestaat en je een dag aan omzet kost.
We draaien een zes-regelaudit op elke Nederlandse SaaS-vendor onder de €25M voordat we een AI-feature in hun stack uitrollen. Het begon als een Miro-sticky-note checklist nadat een rate-limit cliff vorig kwartaal midden in een sprint een klant raakte. Sindsdien heeft de audit twee bijna-outages opgepikt, één stilletjes te dure primary die niemand in negen maanden had herzien, en één vendor wiens "proprietary" model een finetune bleek van iets waar we elders al voor betaalden.
Hier staat wat op de checklist staat, waarom elke regel telt, en welk artifact we aan het eind aan de ops-lead overhandigen.
De cliff en waarom audits urgent werden
Voor wie het miste: het hosted-aanbod van Amazon voor frontier-modellen liep tegen een rate-limit-plafond aan waar veel mid-market SaaS-apps stille afhankelijkheden op hadden gebouwd. De signalen waren subtiel. P95-latency op /chat-routes kroop van 1,8s naar 4,2s. Endpoints voor structured output begonnen ongeveer 1 op 80 calls misvormde JSON terug te geven. Sommige teams patchten het route voor route, vaak zonder de rest van het bedrijf te vertellen dat het primary-model stilletjes naar een kleiner zusje was doorgerold.
De diepere les is niet "zorg voor een fallback". De meeste teams hebben een fallback. De les is dat de meeste SaaS-teams onder de €25M je vandaag niet op papier kunnen vertellen welke routes welk model aanroepen, hoe het responseschema er aan elke kant uitziet, wie de knop mag omzetten, en hoeveel failure-budget per route er eigenlijk is. De audit dicht dat gat.
Er is geen model-gateway, geen vendor en geen inkooptraject voor nodig. Eén folder in je repo en één middag.
Regel één: aantal provider-surfaces
We beginnen met providers en surfaces tellen. Een surface is een aparte manier om een model te bereiken. De API van de vendor zelf is één surface. Een managed cloud-reseller (AWS Bedrock, Google Vertex) is een tweede. Een lokale proxy is een derde. Een interne gateway met een eigen queue is een vierde.
De meeste klanten antwoorden voor de audit "we gebruiken twee providers" en erna "we gebruiken vijf surfaces". Vijf is prima. Vijf ongedocumenteerd is niet prima. We leveren een eenpagina-tabel op met provider, surface, model id, regio, per-minuut tokenplafond, en een link naar de daadwerkelijke rate-limit-documentatie van de vendor waar het getal vandaan komt. Getallen zonder bronlink rotten binnen een kwartaal weg.
De tabel legt ook iets bloot dat teams zelden zien. Twee van je surfaces delen vrijwel zeker een backend. Als je primary één vendor is via directe API en je fallback diezelfde vendor via Bedrock, heb je één provider met twee voordeuren, geen twee providers. Dat is concentratierisico in een vermomming.
Regel twee: per-route fallback-latency
Voor elke productieroute die een model aanroept, meten we twee getallen: de mediane latency op de primary, en de mediane latency op het fallback-pad met een geforceerde primary-failure.
De meting met geforceerde failure is degene die teams overslaan. Het is ook degene die de 9-seconden timeoutcascade vangt waar de primary timeout krijgt, de SDK twee keer met exponential backoff retried, en pas daarna de fallback aanslaat. De gebruiker heeft de tab al gesloten.
We draaien het met een klein harness dat een 503 op de primary injecteert en de route end-to-end volgt:
#!/usr/bin/env bash
set -euo pipefail
ROUTE=$1
SAMPLES=${2:-50}
for i in $(seq 1 $SAMPLES); do
curl -s -o /dev/null -w "%{time_total}\n" \
-H "X-Force-Provider-Fail: primary" \
-H "Content-Type: application/json" \
-d @fixtures/$ROUTE.json \
https://api.your-app.local/$ROUTE
done | sort -n | awk '{a[NR]=$1} END {
print "p50:", a[int(NR*0.5)]
print "p95:", a[int(NR*0.95)]
}'
De X-Force-Provider-Fail header is een development-only switch die je gateway moet honoreren. Als dat niet zo is, is dat finding nummer één. Je kan geen fallback-pad testen dat je niet on-demand kan triggeren.
Regel drie: schema-drift tussen providers
Als je route structured output teruggeeft, stelt de audit één vraag. Als je de primary wisselt, bindt het response dan aan dezelfde Pydantic-, Zod- of JSON Schema-definitie, of vertaalt de integratielaag?
Het eerlijke antwoord voor de meeste stacks is "vertaalt, maar die vertaling is een functie die niemand in zes maanden heeft getest". Schema-drift tussen providers is reëel en stil. Structured Outputs van OpenAI garandeert een strict-schema-modus die de response aan je exacte specificatie vastpint. De tool-use van Anthropic komt dichtbij, maar gebruikt andere regels voor optionele velden en handelt lege arrays net iets anders af. Het function-calling-formaat van Google verschilt weer op enums.
Als je code-pad het ene aanneemt en het andere krijgt, krijg je stille corruptie, geen 500. Stille corruptie is erger dan een 500. Een 500 maakt iemand wakker. Een gedrift veld geeft "Unknown" terug voor het land van een klant en niemand merkt het drie weken lang.
Het audit-artifact is een fixturebestand per route, met de canonieke verwachte output en een klein script dat het daadwerkelijke response van elke provider tegen het schema diff:
import json, sys, jsonschema
from pathlib import Path
schema = json.loads(Path(sys.argv[1]).read_text())
sample = json.loads(sys.stdin.read())
try:
jsonschema.validate(sample, schema)
print("ok")
except jsonschema.ValidationError as e:
print(f"drift: {e.message}")
print(f"at: {'.'.join(str(p) for p in e.path)}")
sys.exit(1)
Pipe de live response van elke provider in het script. De drift komt per route in minder dan een minuut boven water. We draaien het maandelijks tegen vijf bevroren fixtures per provider, wat tegelijk dienst doet als goedkope "is het model zonder ons te informeren doorgerold"-detector.
Een echt voorbeeld uit een van die checks twee maanden geleden. Dezelfde product-extractie prompt naar de primary gestuurd gaf een JSON-array van drie regelitems terug. Dezelfde prompt tegen de fallback gaf één string terug die de drie items met puntkomma's aan elkaar plakte. De downstream consumer verwachtte een lijst, dus produceerde de tweede response stilletjes een order met één fictief product wiens naam drie echte producten aan elkaar geplakt was. Een mens merkte het op voordat onze monitoring het deed, wat de verkeerde volgorde van werken is. De schema-check had het in vijf seconden gevangen.
Regel vier: swap-permissies
Dit is de regel die klanten het meest verrast. We vragen: als je primary-model om 03:00 op een zaterdag tegen de rate-limit aanloopt, wie in je team kan het wisselen zonder dat een backend engineer de repo aanraakt?
Het juiste antwoord is "de on-call ops-lead, via een config-wijziging die binnen 90 seconden van kracht is". Het gangbare antwoord is "niemand, we zouden een PR moeten shippen, CI draaien en opnieuw deployen". Het middenantwoord is "onze CTO, maar alleen zij heeft de sleutel, en ze zit in een meeting".
De fix is mechanisch. Een model-routing config woont in een key-value store, achter een feature flag, met een kleine admin-UI die de huidige primary, de fallback-keten en een one-click rotate-to-fallback knop toont. Twee dagen werk en één nuchtere regel: ops mag 'm omzetten, engineers zijn eigenaar van de canary-checks daarna, en elke flip schrijft een regel naar een audit log.
Het audit log zelf wordt een bijwerking die de moeite waard is. Elke flip schrijft de timestamp weg, de identiteit van de ops-lead, het voor-en-na model id, en de korte redentekst die de operator intypte. Na drie maanden heb je je eigen vendor-reliability dataset. Het volgende inkoopgesprek gaat niet meer over marketingclaims maar over je eigen logs, wat een veel korter gesprek is.
Het vendor-risico is niet dat het model uitvalt. Het vendor-risico is dat het wisselen ervan vastzit achter één persoon die niet in het pand is.
Regel vijf: failure-mode budget per route
Niet elke route verdient dezelfde fallback-machinerie. Een wekelijkse digest-mail tolereert een retry-lus van zes minuten. Een live customer-success chat niet. Een invoice-classifier voor een boekhoudworkflow tolereert een korte rule-based fallback terwijl het model herstelt.
De audit vraagt de product owner, op papier, wat het failure-budget per route is, in seconden en in user-facing tekst. Het artifact is een klein YAML-bestand dat naast de routing-config woont:
routes:
chat.live:
max_total_latency_s: 6
fallback_chain: [primary, secondary, cached_canned_reply]
user_message_on_full_failure: |
We're catching up on a busy moment. Your message is saved
and a teammate will pick it up within the hour.
digest.weekly:
max_total_latency_s: 360
fallback_chain: [primary, secondary, queue_for_human]
user_message_on_full_failure: null
invoice.classify:
max_total_latency_s: 12
fallback_chain: [primary, secondary, rule_based]
user_message_on_full_failure: null
Nu heeft de ops-lead een config die hij kan lezen. De product owner heeft een budget waar hij voor getekend heeft. De engineer heeft een contract. Als er om 03:00 iets breekt, is de vraag niet meer "wat is hier acceptabel" maar "zijn we binnen het budget gebleven waar de product owner mee akkoord ging".
Regel zes: vendor-disclosure check
De laatste regel kwam erbij na een verhaal dat deze maand op Hacker News belandde: het "eigen ontwikkelde" gemeentelijke LLM van een stadsbestuur dat, bij nadere inspectie, sterk leek op een gepubliceerde merge van een bestaande open-weights checkpoint. We gaan niet uit van kwade trouw. We gaan er wel van uit dat vendors zichzelf optimistisch beschrijven, en dat het gat tussen marketingtekst en het daadwerkelijke model is waar de verrassingen wonen.
De check is twee vragen. Eén: publiceert de vendor, op papier, welke modelfamilie elke endpoint aandrijft, en klopt dat met wat ze marketen? Twee: hebben ze in de afgelopen zes maanden een model-rotatie gehad die ze niet hebben aangekondigd?
De tweede vraag kan je beantwoorden door vijf fixture-outputs per route te hashen en ze maandelijks opnieuw te draaien. Als de hash verandert en er was geen release note, heb je een stille rotatie. Dat is niet altijd erg. Het is altijd iets om te weten.
Als een vendor zichzelf "proprietary" noemt en benchmarks komen op vier decimalen overeen met een bekend open-weights checkpoint, is dat geen deal-breaker. Het is iets om te weten voordat je tekent, en zeker voordat je een route bouwt die uitgaat van een specifieke reasoning-stijl of refusal-patroon.
De audit in een middag draaien
Het volledige artifact voor een SaaS onder de €25M past in één repo, één folder:
llm-audit/
providers.md # regel een: surface-tabel
fallback-bench.sh # regel twee: latency-harness
fixtures/
chat.live.json
digest.weekly.json
invoice.classify.json
schema/
chat.live.schema.json
digest.weekly.schema.json
invoice.classify.schema.json
validate-drift.py # regel drie: drift-checker
routing.yaml # regels vier en vijf: wie + budget
vendor-disclosure.md # regel zes: vendorclaims vs realiteit
run.sh # bindt alles samen
Een ops-lead kan elk bestand lezen. Een engineer kan elk script uitbreiden. De audit is geen Notion-pagina die langzaam in een leugen verandert. Het is code die op maandag draait en de maandag daarna ook.
We draaien de volledige pass eens per kwartaal opnieuw, en wekelijks een kleinere cadans: de drift-checker tegen bevroren fixtures, de surface-tabel tegen de live config. Alles vaker dan dat houdt op audit te zijn en wordt monitoring, wat in een andere folder hoort.
Eén klant, één cliff, één weekend
Toen we de inbox-triage agent bouwden voor een Nederlandse logistieke SaaS rond de €8M ARR, was precies dat de rate-limit cliff die midden in de sprint toesloeg, en hun fallback-config woonde in een YAML-bestand dat alleen hun lead engineer kon deployen. We schreven dat weekend de eerste versie van deze checklist, zetten op maandag de routing-config in een key-value store, en trainden hun ops-lead om woensdag modellen te wisselen vanuit een klein admin-paneel: het audit-format hierboven is de versie die we nu hergebruiken voor elke AI-agent die we bouwen.
Het totale werk, inclusief onze tijd en twee middagen aandacht van hun ops-lead, kostte hen ongeveer wat een interne all-hands dag zou hebben gekost. De volgende keer dat de primary tegen de cliff aanliep, drie weken later, duurde de swap 70 seconden, handelde de ops-lead het af zonder de engineer in de camper te pagen, en was de enige Slack-ruis één regel in het routing-audit log. Dat is de vorm van een werkende audit. Hij is saai op de dag dat hij zichzelf terugbetaalt.
De vijfminutenversie die je vandaag kan doen, voordat je verder gaat: open een nieuw document, lijst elke productieroute in je app op die een model aanroept, en schrijf de provider en model id ernaast. Als je een cel niet kan invullen, is dat je finding voor regel één, en de rest van de audit volgt vanzelf.
Kern
Het vendor-risico is niet dat het model uitvalt. Het vendor-risico is dat het wisselen ervan vastzit achter één persoon die niet in het pand is.
FAQ
Hoe vaak moeten we de volledige audit opnieuw draaien?
Eens per kwartaal op een rustige middag, plus elke keer dat je een nieuw model of een nieuwe route toevoegt. Draai de drift-checker maandelijks tegen bevroren fixtures. Vaker dan dat houdt het op audit te zijn en wordt het monitoring.
Hebben we een model-gateway nodig om dit te doen?
Niet strikt. Een YAML-config in een key-value store werkt prima onder de tien routes. Daarboven betaalt een gateway zichzelf terug in observability en de per-route fail-injection switch die je voor regel twee nodig hebt.
Wat als onze vendor niet wil prijsgeven welk model elke endpoint aandrijft?
Dat is een finding, niet altijd een deal-breaker. Combineer het met geen SLA en één facturatiesurface en je hebt concentratierisico dat je niet in het contract hebt verdisconteerd.
Kan een ops-lead echt veilig een primary-model wisselen?
Ja, als de swap een config-wijziging is achter een feature flag met een auto-rollback van 60 seconden en een canary-check. Engineers zijn eigenaar van de canary; ops is eigenaar van de hendel en het audit log.