← Blog

AI agents

Coding agent throttling: negen uur bij een Haarlems bureau

Het coding agent van een Haarlems bureau van 22 mensen ging op een vrijdag in mei negen uur lang door zijn knieën. De throttle zat upstream. De rekening lag bij hen. Dit veranderden we.

Jacob Molkenboer· Oprichter · A Brand New Company· 15 jun 2026· 9 min
Messing relaisschakelaar naast gevouwen papieren telegram en limegroen memo op ivoren bureau met leren onderlegger.

Het is 09:14 op een vrijdag in mei. Een productbureau van 22 mensen in Haarlem opent het sprint board. Vijf engineers, drie designers, twee ops-mensen. Het coding agent dat in hun IDE's zit, draait al vier maanden rustig mee. Om 10:30 doet datzelfde agent er veertig seconden over om een functie af te maken die gisteren in zes seconden klaar was. Om 13:00 maakt het grotere refactors niet meer af, maar het antwoordt nog wel. Het voelt nog levend. Dat is de ergste soort storing.

De maandag erna werden wij erbij gehaald om de post-mortem te schrijven. Dit stuk loopt door negen uur stille degradatie heen, wat er upstream eigenlijk gebeurde, en de drie failover-patronen die wij nu in elk coding agent inbouwen dat we opleveren.

Eerste vermoeden: het netwerk

09:14, nulmeting. Engineer A maakt een Stripe webhook handler af in ongeveer de tijd die je ervan verwacht. Twee diff-suggesties, drie iteraties, klaar voor de koffie.

10:30. Engineer B maakt een ticket aan in de interne Linear van het bureau. "Agent is traag vandaag, doet de kantoor-VPN iets raars?" Drie andere engineers reageren dat het bij hen prima voelt. Zij kijken naar autocomplete, en die is nog niet degraded. De taken met langere context wel.

11:00. De tweede refactor van Engineer A die ochtend loopt vast. De IDE laat de spinner zien. Na negentig seconden komt er een half antwoord terug. Ze proberen het opnieuw. Hetzelfde halve antwoord. Ze geven de branch de schuld.

11:30. Engineer C, werkend in een andere repo, ziet hetzelfde. In het Slack-kanaal komt een klein koor op gang. De senior engineer, met zijn hoofd in een klantdemo, kijkt op en zegt "check de statuspagina van de leverancier." Die is groen. Alles is groen.

12:00. Lunch. De IDE werkt nog, maar niemand vertrouwt hem. Het team begint code in een chatvenster te plakken in plaats van het agent te laten afmaken. Hetzelfde model. Andere surface. Trager dan normaal, maar het werkt. Hypothese: de IDE-plugin is stuk. Na de lunch herinstalleren ze hem.

Zes uur vals alarm

Dit is het deel van de dag dat het meeste geld kost. Niet de throttle zelf. De zoektocht.

Om 14:00 heeft het team twee extensies opnieuw geïnstalleerd, een andere SSH-key geprobeerd, drie Docker daemons herstart en één API-key geroteerd. De senior engineer heeft de statuspagina van de leverancier vier keer gelezen. Nog steeds groen. Ze openen een support-ticket. Het ticket krijgt een geautomatiseerd antwoord. Voor dinsdag horen ze niets meer.

15:00, de demo voor een langlopende klant. De lead engineer wil live een nette refactor doorlopen. Het agent staat zeventig seconden stil voordat er een antwoord uitkomt waarin de helft van de input-context is weggevallen. Ze schakelen over naar slides.

16:00. De lead trekt eindelijk de conclusie: throttle. Voor de rest van de dag zet hij het team op het op één na beste model. Sneller. Ook slechter. Twee engineers gebruiken het agent helemaal niet meer en typen weer met de hand. De interne prijsstelling van het bureau gaat uit van een snelheidswinst van 28% door het agent. Die winst verdampt tussen 09:14 en 17:30.

De schade op die vrijdag, nog voor facturatie of rework, was negen uur keer vijf engineers tegen een gemiddeld tarief van rond de 95 euro per uur. Ruim 4.275 euro aan capaciteit, plus een klantdemo die niet doorging, plus een sprint die uitliep tot de woensdag erna.

Het verhaal upstream

Een deel hiervan is openbaar, een deel hebben we gereconstrueerd uit de logs van het bureau.

Die week had de primaire modelaanbieder van het team stilletjes het tiergedrag aangepast voor een deel van de accounts. Een publieke uitleg is er nooit gekomen. Capaciteitsdruk, een soft rollout van nieuwe throttles, een verkeerd geconfigureerde load balancer, een noisy-neighbor account op dezelfde shard. Kies maar. Wat voor een operations lead telt is niet de oorzaak. Het is de vorm van de verandering.

De throttle gaf geen 429-errors terug. Hij gaf degraded responses terug. Tragere time-to-first-token. Kortere effectieve context windows. Hogere refusal rates op tool calls. Vanuit de IDE kon een gebruiker niet zien of een traag antwoord pech was of stilletjes een tier omlaag was gezet.

De statuspagina van de leverancier liet geen incident zien. De gepubliceerde rate-limit documentatie noemde nog steeds dezelfde tier-getallen. De verandering was echt. Het signaal niet.

Drie failover-patronen die we nu standaard inbouwen

Na de Haarlemse post-mortem hebben we de routing-laag herschreven in elk coding agent en email agent dat we onderhouden. Drie patronen. Alle drie goedkoop. Alle drie gaan ervan uit dat de upstream op enig moment liegt.

Active-passive routing met health beacons

Het eerste patroon is een router die een provider kiest op basis van gemeten gezondheid, niet op een config-flag. Een kleine in-process functie houdt p95-latency en error rate bij over een rolling window van 60 seconden. Elke call werkt de beacon bij. De router leest de beacon voor elke request.

// router/health.ts
type Provider = "anthropic" | "openrouter" | "openai" | "local";

interface Sample { ms: number; ok: boolean; at: number; }
const WINDOW_MS = 60_000;
const log = new Map();

export function record(p: Provider, ms: number, ok: boolean, now: number) {
  const arr = log.get(p) ?? [];
  arr.push({ ms, ok, at: now });
  while (arr.length && now - arr[0].at > WINDOW_MS) arr.shift();
  log.set(p, arr);
}

export function beacon(p: Provider) {
  const arr = log.get(p) ?? [];
  const ok  = arr.filter(s => s.ok).map(s => s.ms).sort((a, b) => a - b);
  const p95 = ok[Math.floor(ok.length * 0.95)] ?? 0;
  const errorRate = arr.length ? 1 - ok.length / arr.length : 0;
  return { provider: p, p95, errorRate, samples: arr.length };
}

De drempels doen ertoe. We zetten p95-latency-plafonds per taaktype, niet per provider. Een refactor over meerdere bestanden op vrijdag 14:00 heeft een andere latency-verwachting dan een autocomplete van zeven regels. Zodra de primaire beacon door het plafond gaat, neemt de volgende provider in de lijst het over voor de volgende request, niet voor de volgende minuut. De IDE wacht niet.

// router/route.ts
const CHAIN: Provider[] = ["anthropic", "openrouter", "openai", "local"];
const CEILINGS = { p95Ms: 8_000, errorRate: 0.10 };

export async function complete(prompt: string, now: number) {
  for (const p of CHAIN) {
    const b = beacon(p);
    if (b.samples < 3 || (b.p95 < CEILINGS.p95Ms && b.errorRate < CEILINGS.errorRate)) {
      const t0 = Date.now();
      try {
        const out = await call(p, prompt);
        record(p, Date.now() - t0, true, Date.now());
        return out;
      } catch (e) {
        record(p, Date.now() - t0, false, Date.now());
        continue;
      }
    }
  }
  throw new Error("all providers degraded");
}

In het Haarlemse geval was het team hiermee om 10:42 al van het primaire endpoint naar een secundair geswitcht in plaats van om 17:30. De secundaire was dezelfde modelfamilie op een andere host. Ze waren negentig minuten kwijt geweest, geen negen uur.

Token budgets per taak met graceful degradation

Het tweede patroon is een budget per taaktype. Autocomplete krijgt een input-budget van 2.000 tokens en een klein model. Refactors krijgen 16.000 input-tokens en een groot model. Explain-this-code krijgt 8.000 en een medium model. Het budget wordt afgedwongen voordat de request weggaat.

// budgets.ts
export const BUDGETS = {
  autocomplete: { inputTokens: 2_000,  outputTokens: 200,   tier: "small"  },
  explain:      { inputTokens: 8_000,  outputTokens: 800,   tier: "medium" },
  refactor:     { inputTokens: 16_000, outputTokens: 4_000, tier: "large"  },
} as const;

export function downshift(tier: "small" | "medium" | "large") {
  if (tier === "large")  return "medium";
  if (tier === "medium") return "small";
  return "small";
}

Dit klinkt als premature optimization tot je ziet wat er gebeurt tijdens een throttle. Als de large tier vastloopt, schakelt de router refactor-jobs terug naar de medium tier in plaats van ze te laten falen of in een timeout te laten lopen. De output is slechter. Het komt nog steeds binnen. De engineer zit in de loop, ziet de downshift in een kleine statusbalk en bepaalt of hij hem accepteert.

We hebben het idee geleend van CDN-failover, waar een degraded ervaring beter is dan helemaal niets. Een lezer wil het artikel nog steeds zien als de image-CDN eruit ligt.

Lokale fallback voor completion-grade taken

Het derde patroon is degene die de meeste teams overslaan en daarna betreuren. Voor autocomplete-achtige taken is een klein lokaal model op de laptop van de engineer goed genoeg. Niet voor een refactor over acht bestanden. Voor een completion van dertig tokens of een suggestie van één regel wel.

We leveren elk coding agent met een lokale modeloptie voorgeladen. De router behandelt het als de laatste schakel in de keten. Het draait ook met een budget van één seconde. Als een remote provider binnen dat budget geen antwoord heeft op een autocomplete, neemt het lokale model het over voor de volgende call.

Het Haarlemse bureau werkte op Apple Silicon laptops met 32 GB RAM. Een coding model van 7 miljard parameters paste daar comfortabel op. Geen van hun engineers had vóór de vrijdagstoring ooit de lokale fallback aangezet, omdat het nooit nodig was geweest. Na de storing zette de senior engineer hem als default voor de eerste dertig minuten van elke werkdag, zodat het team had geverifieerd dat hij werkte.

Lesje

De provider waar je het meest van afhankelijk bent, is degene die het meest waarschijnlijk onzichtbaar uitvalt. Bouw de fallback terwijl je niet bloedt.

De post-mortem op maandag

De volledige audit leverde zes actiepunten op. Twee zijn het waard om hier te benoemen, omdat elk team dat we sindsdien hebben bekeken hetzelfde gat had.

Ten eerste, geen synthetische health check. Het bureau monitorde hun eigen productiediensten met synthetisch verkeer. Hun developer tools monitorden ze niet. Een cron van twee regels die elke vijf minuten een bekende prompt naar het coding agent stuurt en alarmeert bij een p95 boven vijf seconden had de throttle om 10:30 gevonden, niet om 17:30.

#!/usr/bin/env bash
# every 5 min via cron, alert if p95 of last 10 runs > 5s
start=$(date +%s%3N)
curl -sf --max-time 30 "$CODING_AGENT_URL/complete" \
  -H "content-type: application/json" \
  -d '{"task":"healthcheck","prompt":"return the string OK"}' \
  -o /tmp/agent.out
end=$(date +%s%3N)
echo "$((end - start))" >> /var/log/agent-latency.log
tail -10 /var/log/agent-latency.log | sort -n | awk 'NR==9{ if ($1>5000) exit 1 }'

Ten tweede, geen exit ramp voor het team. Zodra het agent kapot leek, bepaalde elke engineer zelf hoe hij eromheen werkte. Sommigen bleven retryen. Sommigen stopten er helemaal mee. Sommigen plakten in een chatvenster. De vorm van die beslissing hoort in een runbook van één pagina, niet in vijf losse hoofden. Die runbook past op een notitiekaart: welke provider als volgende, welk taaktype pauzeren, wie het sein veilig geeft.

Het bureau draait nu beide. De synthetische check is het script van twaalf regels hierboven. De runbook is één alinea, vastgepind in het dev-kanaal.

Waar je maandag begint

Het patroon is niet moeilijk. De kosten van een incident zoals dat in Haarlem zitten vooral in het gat tussen het moment dat het probleem begon en het moment dat iemand het benoemde. Een health check van vijf minuten die een bekende prompt afvuurt en alarmeert als de responstijd door een plafond gaat, brengt je het grootste deel van de weg. Voeg een tweede provider toe in dezelfde keten. Pin een runbook van één alinea vast in het dev-kanaal. De rest bouw je in de weken die volgen.

Toen wij de failover-laag voor het Haarlemse bureau bouwden, had de bestaande IDE-plugin geen hook om providers per request te wisselen. We hebben dus een kleine lokale proxy gemaakt waar de plugin naar wees, en lieten die proxy elke routing-beslissing nemen. Dat soort leidingwerk is het grootste deel van wat we doen binnen onze AI-agents praktijk.

Kern

De provider waar je het meest van afhankelijk bent, is degene die het meest waarschijnlijk onzichtbaar uitvalt. Bouw de failover terwijl je niet bloedt.

FAQ

Hoe zag de throttle er anders uit dan een normale trage response?

Tragere time-to-first-token, kortere effectieve context en hogere refusal rates op tool calls. Geen HTTP-foutcodes terug en geen incident op de statuspagina, dus de IDE leek gewoon levend.

Wat is een synthetische health check voor een coding agent?

Een kleine cron-job die elke paar minuten een bekende prompt naar het coding agent stuurt en alarmeert als de p95-responstijd door een plafond gaat. Twaalf regels bash zijn genoeg om te beginnen.

Is een lokale model-fallback realistisch op een normale laptop?

Voor autocomplete en suggesties van één regel op Apple Silicon laptops met 32 GB RAM, ja. Voor refactors over meerdere bestanden, nee. Behandel het als de laatste tier in de keten, niet als dagelijkse keuze.

Waarom niet vanaf dag één gewoon meerdere providers gebruiken?

De meeste teams zouden dat moeten doen. De wrijving zit meestal in de IDE-pluginlaag, die maar één endpoint blootstelt. Wij leveren vaak een kleine lokale proxy waar de plugin naar wijst en stoppen de routing-logica in die proxy.

ai agentstoolingarchitectureoperationscase study

Iets bouwen?

Start een project