AI agents
Lokale code-LLM's: hoe een dierenartsketen €1.840 schrapte
De pager ging af om 02:14. De RTX 5090 in het bezemkast-rack was OOM gegaan tijdens een PR-review, en de dienstdoende dev zat in de trein naar Utrecht met acht reviews op de stapel.

De pager om 02:14
Om 02:14 op een dinsdag in maart maakte de wachtpager Lieke wakker. De RTX 5090 die in een omgebouwde bezemkast op het Nijmeegse hoofdkantoor van haar werkgever stond, was OOM gegaan tijdens een PR-review. Lieke is een van de drie developers die de Animana-koppelingen onderhouden voor een keten van negen dierenartspraktijken in Gelderland. Het runbook zei: kill de container, wacht op de watchdog, zet opnieuw in de wachtrij. Het duurde vier minuten. Ze noteerde het incident vanaf een perron en sliep verder.
Zes maanden eerder waren die acht openstaande reviews door Claude afgehandeld. Niemand zou wakker zijn geworden. De rekensom waarom deze keten besloot dat wakker worden de betere ruil was, volgt hieronder.
Een post van €1.840
De keten heeft 22 medewerkers. Drie daarvan zijn developer. Het dev-team beheert een codebase van ongeveer 140.000 regels PHP en TypeScript. Die wikkelt het Animana praktijkmanagement-systeem van IDEXX in een eigen boekingsfront-end, een SOAP-naar-REST-shim voor labuitslagen, een facturatieroute en een rij cron jobs die no-shows tegen de agenda afstrepen. De Animana-kant is elf jaar oud. Sommige cron jobs zijn ouder.
Sinds oktober 2025 draaide een LLM als code-review-assistent op elke PR plus een interactieve sidecar in hun editors. De maandelijkse Claude-rekening pendelde rond €1.840. Voor een keten met een IT-budget van ongeveer €11.000 per maand inclusief volledige developer-kosten, was dat zo'n 17% van die post. De boekhouder kaartte het twee keer aan. De dev lead beloofde ernaar te kijken.
'Ernaar kijken' werd een inkoopvraag op de middag dat de voorpagina van Hacker News een Ask HN naar boven duwde over Claude vervangen door lokale modellen voor dagelijks coderen. Eén van de developers deelde de link in de team-Slack.
De hardware-shortlist
De shortlist kwam neer op drie opties. Een tweedehands paar RTX 3090's op een workstation-bord. Eén RTX 5090 met 32 GB GDDR7 in een Threadripper-bak. Of een rack-mount H100 in een Frankfurt-colo. De H100 kostte twee keer een jaar Claude. Binnen tien minuten was die uit de spreadsheet geschrapt.
Het 3090-paar was goedkoop en had een 30B-model met fatsoenlijke throughput kunnen draaien. Het waren ook twee consumer-GPU's in een metalen bak in een pand dat in de zomer wel eens zonder airco komt te zitten. Ze kozen voor de 5090 om twee redenen: een single-card thermisch profiel dat de bezemkast aankon, en genoeg VRAM-marge voor een 30B-model op 4-bit met een comfortabele KV cache voor de langste review-prompts van het team.
Totale uitgave, inclusief de machine, de GPU, een kleine UPS en een eenmalige middag werk van een elektricien om een eigen 16 A-groep naar de kast te trekken, kwam net onder de €4.900. Terugverdientijd ten opzichte van de Claude-post was op papier 2,7 maanden.
Waarom Qwen3-Coder 30B, en niet iets groters
Ze testten drie open-weights kandidaten op een sample van vijftig echte PR's uit het vorige kwartaal. De kandidaten: een 70B general-purpose model op 4-bit, een 30B code-gespecialiseerd model en een 14B model op volle precisie. De maatstaf: zou een senior engineer de review als bruikbaar accepteren, zonder vals alarm op Animana-specifieke patronen die het model nooit tijdens training had kunnen zien.
Het 70B-model was het meest welbespraakt. Het was ook het traagst en miste de meeste Animana-idiomen. Het 14B-model was snel en stellig fout over SOAP-envelopes. Het 30B code-gespecialiseerde model, in dit geval Qwen3-Coder 30B, zat op de plek die telde: het las de verouderde PHP zonder mokken, het ving precies de off-by-one cron-foutjes die het team echt maakt, en het draaide snel genoeg op de 5090 dat de round trip van 'PR open' naar 'eerste comment' onder de vijftien seconden bleef voor typische diffs.
Het is niet het beste code-model ter wereld. In ons eigen werk grijpen we voor de zwaardere architectuurgesprekken nog steeds naar de frontier-API's. Voor de acht tot twintig PR-reviews die dit team per dag doet tegen een stabiel legacy-oppervlak, was de 30B goed genoeg dat ze het verschil binnen twee weken niet meer opmerkten.
PR-review latency, eerlijk gemeten
We logden vier cijfers per PR, negentig dagen voor en negentig dagen na de overstap.
- Tijd van PR-open tot eerste comment van het model, cold cache.
- Tijd van PR-open tot eerste comment van het model, warme cache.
- Aantal comments waar de menselijke reviewer het mee eens was.
- Aantal comments die de menselijke reviewer als ruis markeerde.
De kopregel-uitkomst is saai, en dat is precies de bedoeling. Cold-cache p50 latency ging van 6,1 s op de API naar 11,4 s lokaal. Warme cache ging van 3,2 s naar 4,8 s. Bruikbare comments per PR zakten van 4,7 naar 4,1. Ruis-comments per PR stegen van 0,6 naar 0,9. De dev lead las de cijfers zo: het lokale model voegde ongeveer vijf seconden naar-het-tabblad-staren per PR toe en één extra 'nee, dat klopt'-klik per PR. In ruil daarvoor kregen ze hun €1.840 terug.
De juiste vraag over een lokaal model is niet 'is het zo slim als de frontier?'. Het is 'is het goed genoeg dat het team het verschil binnen twee weken niet meer opmerkt?'. Meet beide cijfers. Vertrouw het tweede.
De vastloper van 02:00 en het runbook dat eruit kwam
In de eerste zes weken liep de bak drie keer vast. Twee keer op een lange PR die over de ingestelde context heen ging, één keer op iets dat eruit zag als een echte driver-hang. De fix was in alle drie de gevallen dezelfde: herstart de inference-container onder systemd, wacht tot de watchdog hem als healthy markeert, zet de openstaande reviews opnieuw in de wachtrij. De CI-pipeline was al idempotent, dus opnieuw inzetten kostte niets.
Het runbook past op één geprint A4. De systemd-unit is het dragende stuk.
# /etc/systemd/system/qwen-coder.service
[Unit]
Description=Qwen3-Coder 30B local inference (vLLM)
After=network-online.target
[Service]
ExecStart=/usr/local/bin/vllm serve Qwen/Qwen3-Coder-30B \
--max-model-len 32768 \
--gpu-memory-utilization 0.92 \
--enable-prefix-caching \
--served-model-name qwen3-coder
Restart=on-failure
RestartSec=15
WatchdogSec=120
TimeoutStopSec=30
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target
Twee details om te jatten. De watchdog van 120 seconden vangt een vastzittende NVIDIA-driver eerder op dan de developers. De 92% memory-utilisation laat genoeg VRAM-marge voor de prefix cache zonder bij de langste prompts tegen OOM aan te schurken. Dat tweede leerden ze op de harde manier na de 02:14 van Lieke.
Een AVG-auditlog dat een visitatie overleeft
Nederlandse dierenartspraktijken worden ongeveer jaarlijks gevisiteerd door de KNMvD, en elke praktijk die met klantgegevens werkt moet haar dataflows kunnen uitleggen aan de Autoriteit Persoonsgegevens onder de AVG. De functionaris gegevensbescherming van de keten was vanaf dag één beleefd sceptisch over de Claude-post. Patiënt-aangrenzende debug-context naar een Amerikaanse API sturen was technisch geen datalek, maar het was het soort ding dat de FG niet wilde hoeven uitleggen tijdens een visitatie.
Lokaal gaan draaide dat gesprek om. Elke inference-request die de bak beantwoordt wordt weggeschreven naar een append-only bestand op een aparte schijf, dagelijks geroteerd en elke zondag naar cold storage gezet. Elke entry is één JSON-regel.
{
"ts": "2026-03-11T02:14:07Z",
"request_id": "pr-2841-review-3",
"actor": "ci-bot@clinic.local",
"model": "qwen3-coder-30b",
"input_sha256": "7af9c1...e22b",
"patient_data_detected": false,
"redactions_applied": [],
"tokens_in": 4812,
"tokens_out": 1166,
"latency_ms": 8740,
"exit": "ok"
}
Twee dingen tellen hier. De hash van de input betekent dat ze achteraf kunnen bewijzen wat er verstuurd is, zonder de prompt zelf op te slaan. De patient_data_detected-vlag is de uitkomst van een kleine regex-plus-NER-stap die draait voor de prompt het model raakt. Elke hit krijgt de bijhorende span geredigeerd en de redactie wordt vastgelegd. Tijdens de visitatie in mei 2026 vroeg de inspecteur om één willekeurig gekozen week aan logs. De FG exporteerde de JSONL, draaide er één regel jq overheen om het redactiepercentage te laten zien, en het gesprek schoof door naar de vraag of de chemicaliënkast op slot zat.
Wat nog naar de API gaat
Het team hield een Claude-budget van €120 per maand aan voor wat zij 'de moeilijke' noemen. Architectuurgesprekken die een model nodig hebben dat meer van de wereld heeft gelezen. Een incidentele uitwijk wanneer de lokale bak gepatcht wordt. Twee van de drie developers betalen daarnaast persoonlijk voor een chat-abonnement, wat het bedrijf niets aangaat.
De post komt niet meer ter sprake in financieel overleg. De dev lead krijgt er geen vragen meer over, en dat is de meest eerlijke graadmeter of intern gereedschap werkt.
Toen wij de lokale-inference uitrol bouwden voor deze keten, bleven we vastlopen op de redactiestap. Die moest snel genoeg zijn om de PR-latency niet te verdubbelen, maar grondig genoeg om de FG gerust te stellen. We hebben het opgelost met een klein NER-model op de CPU plus een regex-pas voor Animana-specifieke patiënt-ID-formaten. Het is precies het soort legacy-bewuste lijmwerk waar het meeste van ons werk aan AI-agents op neerkomt.
Het kleinste wat je deze week kunt doen
Voor je een GPU gaat doorrekenen, draai eerst twee weken telemetrie. Log elke prompt die je team naar een hosted API stuurt, hash hem, tel de tokens, markeer de prompts die data raakten waar je FG iets van zou vinden. Na twee weken weet je twee dingen die je nu niet weet: hoeveel van je uitgave gaat naar werk dat een lokaal 30B-model plausibel ook kan, en hoeveel naar de echt moeilijke problemen die de API-post rechtvaardigen. De beslissing valt vanzelf uit het histogram.
Kern
Een Claude-post van €1.840 per maand werd een eenmalige bak van €4.900 en drie vastlopers in zes weken. Meet eerlijk voor je overstapt en houd een API-budget aan voor de moeilijke problemen.
FAQ
Hoeveel VRAM heb je nu echt nodig voor Qwen3-Coder 30B?
Op 4-bit quantisatie past een 30B code-model in ongeveer 20 GB met ruimte voor prompt cache. Met de 32 GB van de RTX 5090 houd je lange contexten warm zonder te tunen.
Is broncode naar een hosted LLM sturen een AVG-probleem?
Op zichzelf niet. Het wordt een probleem zodra prompts persoonsgegevens bevatten en je FG niet kan laten zien wat er is verstuurd. Een gehashte, append-only auditlog draait dat gesprek om.
Wat doet de bak als er geen PR's gereviewd worden?
Niets. Hij idlet. Verbruik bij idle is rond de 30 W op de 5090. Geen wachtrij, geen inference, geen logregels, geen warmte in de kast.
Miste het team Claude na de overstap?
Niet voor routinematige PR-review, binnen twee weken al niet meer. Wel voor diepere architectuurklussen. Daarom houden ze een klein maandelijks API-budget aan voor de moeilijke problemen.
Wat ging er als eerste stuk, en hoe is het opgelost?
De bak ging OOM op een lange PR die over de ingestelde context heen schoot. De fix was GPU-memory utilisation terugzetten naar 92% en de systemd-watchdog de container laten herstarten.