AI agents
AWS Bedrock datavoorwaarden: anatomie van 38 uur storing
Om 19:42 op een dinsdag zag een legal-tech team van 24 mensen in Nijmegen hoe hun contractclassifier midden in een deployment stilviel. Dit brak er, en zo liepen ze 38 uur terug.

Het is 19:42 op een dinsdag in maart. De on-call engineer bij een legal-tech SaaS van 24 mensen in Nijmegen pusht een routinedeploy: twee nieuwe clausule-templates, één parser-fix. CI is groen. De canary is groen. Twee minuten later komt elk contract dat naar staging wordt geüpload terug met dezelfde vorm: een lege array waar tien of twaalf geclassificeerde clausules zouden moeten staan.
Het product is een contract-review tool voor middelgrote Nederlandse advocatenkantoren. De classifier neemt een PDF, haalt de tekst eruit en vraagt een gehost model om elke clausule te labelen: geheimhouding, vrijwaring, toepasselijk recht, opzegging voor gemak, het bekende landschap. De labels voeden een reviewer-UI. Zonder labels is die UI een blanco pagina.
Om 20:10 heeft het team teruggerold naar de vorige deploy. Dezelfde lege arrays. Om 20:35 hebben ze nog twee deploys teruggerold. Dezelfde lege arrays. De on-call pingt de founder. De storing die volgt duurt 38 uur, en de oorzaak blijkt in een service-terms PDF te zitten die niemand in het team had gelezen.
Wat er binnen Bedrock was veranderd
Wat het team nog niet wist: Amazon had drie dagen eerder een update gepubliceerd van de Bedrock service terms. De wijziging verbreedde de set Anthropic-modellen waarvoor AWS prompts en completions van klanten kon doorsturen naar Anthropic voor evaluatie en toekomstige modeltraining, tenzij het account een specifieke opt-out flag had gezet bij elke model-invocatie. De wijziging was die week al op de HN-frontpage gekomen, maar niemand in het team had die kop aan zijn eigen stack gekoppeld.
Hun AWS-account stond niet op opt-out. De Terraform-module die de Bedrock provisioned-throughput unit had aangemaakt was acht maanden eerder geschreven, voordat het opt-out mechanisme bestond. De voorwaardenwijziging was maandag van kracht geworden. Hun classifier, die een Claude-model via Bedrock aanriep met prompts waarin letterlijke clausules uit klantcontracten zaten, begon te falen op een server-side compliance check die een HTTP 200 met een lege content array teruggaf.
200 OK. Lege content. Geen error. Geen logregel om op te greppen.
Managed inference providers kunnen hun voorwaarden wijzigen en server-side afdwingen zonder dat er een error in je SDK terechtkomt. Een 200 met een lege payload is de ergste soort failure: je code denkt dat de call slaagde, je monitoring denkt dat de call slaagde, en je gebruikers zien een blanco pagina.
Waarom de failure leek op onze eigen code
De classifier-wrapper was defensief geschreven. Als het model een lege array teruggaf, logde de wrapper "no clauses detected" en gaf een lege array terug aan de caller. De wrapper was getest tegen kapotte PDF's en tegen contracten in talen waar het model slecht mee omging. Lege array betekende "we hebben het geprobeerd, er kwam niets bruikbaars terug". De deploy van die avond had de parser aangeraakt. De on-call engineer ging er redelijkerwijs vanuit dat de parser de boosdoener was. Datzelfde gold voor de drie engineers die daarna gepiept werden.
Ze brachten elf uur door met het bisecten van de parser-wijziging voordat iemand bedacht om dezelfde prompt direct naar Bedrock te sturen met curl. Dezelfde lege content. Ze probeerden een hardcoded prompt uit de unit-test fixtures, ook uit een echt contract gehaald. Dezelfde lege content. Toen probeerden ze het kinderboek-voorbeeld uit de AWS-docs. Volledige response, normale completion. Dat was uur 14.
Daarna stuurden ze dezelfde fixture-contract prompt via een persoonlijke Anthropic-key die een van de engineers voor een nevenproject had, rechtstreeks naar het model. Volledige response. De clausules kwamen correct gelabeld terug. Uur 15. Op dat moment wist het team dat het probleem upstream van hun code zat, niet erin.
Uur 18, weer in beweging
Tegen uur 18 stond de werkhypothese vast: het model weigerde prompts die klantcontracttekst bevatten, en de Bedrock-wrapper at die weigering op tot een lege array in plaats van die door te geven als een gestructureerde error. Ze openden een supportcase bij AWS. Het eerste antwoord kwam zes uur later en wees ze op de geüpdatete service terms en een invocation-level flag die ze nooit hadden gezet.
Dat was uur 24.
Het team had twee opties. De flag zetten bij elke model-call, opnieuw deployen, en hopen dat er verder niets stilletjes in de voorwaarden was verschoven. Of overstappen op een directe Anthropic API-key met een EU-data-residency addendum en stoppen met afhankelijkheid van de Bedrock-wrapper.
Ze kozen optie twee om drie redenen. De contracten die ze verwerkten vielen onder Nederlands recht en bevatten regelmatig AVG-gevoelige clausules. Hun general counsel voelde zich ongemakkelijk bij impliciete trainingsdata-stromen richting de VS, zelfs met de flag gezet, omdat de audit trail van de voorgaande dagen niet meer te herstellen was. En het team plande de overstap weg van Bedrock al zes maanden en had een half afgemaakte migratiebranch al gepusht.
De cutover
Die halfklare branch hielp. De volledige cutover was nog steeds geen swap van vijf minuten.
Wat er moest veranderen:
- De SDK-call.
boto3.invoke_modelwordt een directe Anthropic SDK-call. - Authenticatie. Een IAM-rol voor Bedrock wordt een API-key, opgeslagen in hun bestaande secrets manager en geroteerd op hetzelfde schema als hun database-credentials.
- Region pinning. De directe Anthropic processing-region wordt vastgezet op de EU onder hun commerciële overeenkomst en een verwerkersaddendum, niet in code geconfigureerd.
- Retry en backoff. Bedrock heeft zijn eigen throttling-verhaal. Directe Anthropic gebruikt andere rate-limit headers en een andere 429-cadans.
- Cost accounting. Het team had een Bedrock-dashboard aangesloten op Grafana. Vervangen door het Anthropic billing endpoint en hetzelfde Grafana-paneel.
De classifier-wrapper was dun, wat hielp. Het verschil tussen voor en na valt amper op:
import boto3, json
client = boto3.client("bedrock-runtime", region_name="eu-central-1")
resp = client.invoke_model(
modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
body=json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"messages": messages,
"max_tokens": 2048,
}),
)
body = json.loads(resp["body"].read())
clauses = parse_clauses(body["content"])
import os
from anthropic import Anthropic
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
# Processing region is pinned to the EU under the data-residency
# addendum in the commercial agreement, not in this call.
resp = client.messages.create(
model="claude-3-5-sonnet-20241022",
messages=messages,
max_tokens=2048,
)
clauses = parse_clauses(resp.content)
De cutover zelf duurde vier uur. Ze deden het achter een feature flag, schoven verkeer in stappen van 10%, en keken naar de per-clausule labeldistributies voor drift tegen een ingevroren golden-set van 200 contracten. Als de labeldistributie bij 10% er anders uitzag dan de Bedrock-distributie bij 10%, hadden ze teruggetrokken. Dat hoefde niet. Op uur 38 vanaf het begin van de storing draaiden ze op 100% via het directe pad.
Wat er daarna permanent veranderde in de architectuur
Drie dingen werden permanent na het incident.
Het eerste is een synthetische health check die elke vijf minuten draait tegen het productie-inference endpoint. De check stuurt een prompt die eruitziet als echte contracttekst maar geen klantdata bevat. De check controleert het schema en een ruwe cardinaliteit (tussen 4 en 20 clausules) op de response. Elke drift triggert een alert voordat een echt contract het endpoint raakt. Deze ene wijziging had de 38 uur teruggebracht tot vijf minuten. Het is ook het goedkoopste op de lijst.
Het tweede is dat de wrapper nu onderscheid maakt tussen lege array en null. Lege array betekent "we kregen een bruikbare response van de provider die nul clausules bevatte, het contract was waarschijnlijk een coversheet". Null betekent "de provider gaf geen bruikbare response terug, kijk nu meteen upstream". De reviewer-UI behandelt die anders. Null toont een incident-banner en pauzeert uploads. Lege array toont "geen clausules gevonden, beoordeel handmatig". Twee maanden later heeft dit onderscheid nog twee provider-side incidenten gevangen die geen storing zijn geworden.
Het derde is dat de classifier niet langer afhankelijk is van één inference provider. De wrapper heeft een primair pad (direct Anthropic) en een fallback (een kleinere open-weights classifier op hun eigen GPU). De labels van de fallback zijn ruiziger en de reviewer-UI maakt dat zichtbaar voor de jurist die ermee werkt. De fallback werd voor het eerst live aangesproken bij een geplande oefening, drie weken na de cutover. Het werkte. Het team heeft nog niet onder druk hoeven terugvallen.
Wat we het team om middernacht zouden vertellen
Als je inference in een managed wrapper draait, bezit je niet het contract tussen jou en het model. Je bezit het contract tussen jou en de wrapper. Wanneer de wrapper upstream heronderhandelt, hoor je dat door release notes te lezen waar je niet op geabonneerd was, of doordat je product om 19:42 op een dinsdag stilvalt.
Dit is geen argument tegen managed inference. Bedrock, Vertex en Azure OpenAI verdienen hun plek met procurement, billing, IAM, en één factuur in plaats van drie. Het is een argument om het model achter de wrapper te behandelen als een externe afhankelijkheid op dezelfde manier waarop je een payment processor of een email-provider behandelt: met een health check, een gedocumenteerde fallback, en een kwartaalreview van de voorwaarden waar je feitelijk onder werkt.
Toen we eerder dit jaar de email-agent bouwden voor een Rotterdamse logistieke firma, was de failure die ons het langst kostte om te diagnosticeren identiek van vorm: een inference-call die 200 OK met lege content teruggaf, geen provider-error, geen logregel. We zijn uitgekomen op hetzelfde synthetische health-prompt patroon, en een harde regel dat elke empty-but-successful response een alert triggert voordat hij ooit een gebruiker bereikt. Als je AI-agents in productie zet, doen de stille failures de meeste pijn.
Heb je deze week één inference-call in productie staan, schrijf dan een health-check van vijf regels die elke minuut hetzelfde model, dezelfde regio en dezelfde prompt-vorm raakt, en alert op elke response die niet matcht met je schema. Dat alleen had deze storing in onder de vijf minuten gevangen.
Kern
Als een managed inference provider zijn voorwaarden wijzigt, is de failure-modus stilte, geen error. Health-check je model-endpoints zoals je je database health-checkt.
FAQ
Was dit een Bedrock-bug of een voorwaardenwijziging?
Een voorwaardenwijziging met server-side handhaving. De compliance check gaf een lege content array terug in plaats van een weigering of een error, waardoor de SDK en de wrapper de call beide als een normale success behandelden.
Had een opt-out flag de storing kunnen oplossen zonder over te stappen?
Ja. De gedocumenteerde flag zetten bij elke model-invocatie had de dienst binnen een uur hersteld. Het team koos om Bedrock te verlaten om losstaande AVG- en provider-onafhankelijkheidsredenen die al maanden in de wachtrij stonden.
Biedt Anthropic direct EU data-residency?
Ja, onder een commerciële overeenkomst en verwerkersaddendum. Region pinning wordt in het contract onderhandeld en op platformniveau gehandhaafd, niet in de SDK-call zelf geconfigureerd.
Wat is het kleinste wat je deze week kunt doen?
Voeg een synthetische health-prompt toe die elke minuut tegen je productie-inference endpoint draait en alert op schema-drift of empty-but-successful responses. Vijf regels code, vangt de stille failures.