Case Study: AI Agent Platform
Requirements
- Accept a task, return immediately, and let the work run for minutes without holding a connection.
- Call a hosted model API with credentials that are rotatable and never present in an image or a repository.
- Execute model-chosen tools — including generated code — without giving them the platform's own privileges.
- Ground answers in the customer's documents, isolated per tenant.
- Attribute cost per run and per tenant, and stop a run that exceeds its budget.
- Explain, after the fact, what a run did: every step, every tool call, every token.
Deliberately not requirements
Half of a design is what it refuses to do. These are the refusals.
- No model training or fine-tuning — this platform consumes models, it does not produce them.
- No sub-second responses: the product's promise is minutes of autonomous work, not chat latency.
- No self-hosting of the main reasoning model at this stage; that is a deliberate later decision with its own economics.
How the design got here
In order. Each stage leads with the problem that forced it.
Call the model inside the request
The requirement, in its first form: a single-turn assistant endpoint. One model call, three seconds, a response. There is no queue, no worker, no run state and no tracing, and for single-turn question answering this remains the right design far longer than most teams believe. A great deal of AI infrastructure is built past this point without the problem that justifies it.
| Decision | Reason | Alternative | Trade-off |
|---|---|---|---|
| Synchronous, in-request model calls. | One call that fits inside an HTTP timeout needs no orchestration. Everything after this stage is machinery for work that does not fit in a request. | Asynchronous runs from the start, which is more infrastructure than the feature and delays learning whether anyone wants it. | Model latency is now request latency, and it is variable in a way ordinary backends are not — a slow provider minute is a slow product minute, and there is nothing you can do about it from inside the request. |
| Aggressive timeouts and a circuit breaker on the model call from day one. | The model provider is a third party on the critical path of every request. Without a bounded timeout, a provider slowdown becomes thread-pool exhaustion and takes down endpoints that never call a model. | A generous timeout so long requests can finish, which converts a provider incident into a full outage of your API. | Some legitimately long generations are cut off, and the timeout has to be re-tuned whenever prompts or models change. It is a permanent maintenance item. |
Runs as durable objects on a queue
The assistant became multi-step. A task calling three tools and the model six times took four minutes; the load balancer closed the connection at sixty seconds, the client treated it as a failure and retried, and the retry executed the same tool side effects a second time — including sending a customer email twice.
| Decision | Reason | Alternative | Trade-off |
|---|---|---|---|
| A run is a durable object with an idempotency key, not a request. | Work that outlives a connection needs an identity. The key makes a client retry return the existing run instead of starting a second one that repeats every side effect. | Fire-and-forget background threads in the API process, which needs no new components and loses every in-flight run on the next deploy. | A state machine to design, store and expose in the API contract, plus the honest question of what a "partially completed" run means to a user — a question with no clean answer when the steps had external side effects. |
| Checkpoint after every step, and make steps individually replayable. | Steps are minutes long and expensive. A worker lost at step five should resume at step five, not re-run four model calls you have already paid for. | Restart the run from the beginning on failure, which is far simpler and multiplies both cost and latency on every interruption. | Every step must be checkpointable, which constrains how the agent loop is written, and replaying a step whose side effect already happened requires the tools themselves to be idempotent — application work, not infrastructure. |
| Queue visibility timeout longer than the longest step, with a heartbeat. | A run that takes minutes will otherwise be redelivered mid-flight and executed twice concurrently, which for an agent means duplicated tool calls in the real world. | A short timeout, which recovers faster from crashes and duplicates long-running work. | A long timeout delays recovery from a genuine worker crash; the heartbeat mitigates that and adds a failure mode of its own, where a hung worker keeps heartbeating a run that is going nowhere. |
Model credentials as managed secrets with spend caps
The model API key was baked into the worker image, so anyone with registry pull access had it — and the image was mirrored into a staging account with wider access. The realization that landed: a leaked model key is not only a data-access problem, it is a *metered billing instrument*. A stolen key is somebody spending your money at machine speed.
| Decision | Reason | Alternative | Trade-off |
|---|---|---|---|
| An internal model gateway that is the only holder of the provider key. | It gives one place to rotate the credential, one place to enforce per-tenant budgets and rate limits, one place to count tokens, and one place to swap providers. Without it, every one of those becomes a change in every worker. | Each worker calls the provider directly with its own credential, which is one less hop and scatters the key, the accounting and the rate limiting across the fleet. | A new component on the critical path of every inference call, which must be highly available and adds latency. It is also a chokepoint: a bug in it stops all AI functionality at once. |
| Separate keys per environment, with independent spend caps. | It bounds the financial blast radius of a leak and stops a staging load test from consuming the production budget — a genuinely common way to discover you had one budget. | A single organizational key, which is simpler to manage and makes every incident a full-spend incident. | More keys to rotate, more places for a mismatch to cause a confusing "unauthorized" in one environment only. |
| Alert on spend *rate*, not only on cumulative spend. | A monthly budget alert fires long after a leaked key or a runaway loop has done its damage. Tokens per minute against a baseline detects both within minutes. | Provider budget alerts on the monthly total, which are free, easy, and lagging by days. | A rate baseline must be maintained as usage grows, and it will page during legitimate spikes — a customer running a large batch looks exactly like a runaway loop until someone looks. |
Tool execution in a sandbox
A tool that executed model-generated code ran inside the worker process, with the worker's network access and the worker's identity. That put the platform one prompt injection away from an agent reading the production database credential out of the environment and exfiltrating it through an ordinary outbound HTTP call. Nothing had been exploited; the design simply had no boundary in it.
| Decision | Reason | Alternative | Trade-off |
|---|---|---|---|
| Model-influenced execution happens in a separate compute boundary with no ambient credentials. | Treat every tool input as attacker-controlled, because a prompt injection in a customer document makes it exactly that. The only durable defence is that the code has nothing worth stealing in its environment. | Run tools in-process with careful input validation, which is faster and cheaper and relies on being able to enumerate every hostile input — the assumption that fails. | Per-call sandbox startup adds latency to every tool invocation, and you now operate a container isolation story with its own patching and escape surface. This is the most expensive stage in the design, in both compute and complexity, and it is the one that is not optional. |
| Egress from the sandbox goes through an allowlist proxy. | Exfiltration needs a path out. An allowlist turns "the agent can call anything" into "the agent can call the four things we approved", and every denied attempt becomes a signal. | Open egress with monitoring, which detects exfiltration after it has happened. | Every new tool integration needs an allowlist change, which is friction that engineers will route around if the process is slow. The proxy is also a single point of failure for all tool calls. |
| Hard CPU, memory and wall-clock limits per tool call, enforced by the platform. | Generated code loops. Without a limit, one tool call can hold an instance indefinitely, which is both a cost incident and a capacity incident. | Trust the tool implementation to bound itself, which works for the tools you wrote and not for the code the model wrote. | Legitimate long-running tools must be redesigned as asynchronous jobs of their own, and the limits need tuning per tool — a permanent source of "it works locally" reports. |
Retrieval over customer documents
Answers were confidently wrong because the model had no access to the customer's own material. Putting the whole corpus in the prompt was impossible for large tenants and, for the ones where it fit, made a single run cost more than the customer paid in a month.
| Decision | Reason | Alternative | Trade-off |
|---|---|---|---|
| Tenant isolation is enforced at query time and asserted in tests. | A vector index does not naturally respect tenancy — nearest-neighbour search will happily return another customer's chunk. A missing filter here is a cross-tenant data leak delivered inside a plausible-sounding answer, which is the hardest kind to notice. | An index per tenant, which is unambiguous and expensive, and hits collection limits at scale — the right answer for a small number of large, sensitive tenants. | Shared indexes mean the filter is load-bearing, so it needs tests that assert a query *cannot* return another tenant's data, plus a review discipline on every query path. |
| The vector index is a stateful service with a memory-shaped cost, treated like a database. | It needs capacity planning, backups, version upgrades and a rebuild plan. Teams routinely treat it as a library and are surprised when it becomes the second most expensive and least understood component they run. | A vector extension inside the existing PostgreSQL, which removes an entire component and is entirely adequate at moderate corpus sizes — genuinely the first thing to try. | A dedicated index scales further and costs a permanent always-on memory footprint. Rebuilding it after a model change means re-embedding the whole corpus, which is a large one-off bill nobody budgets for. |
| Re-embedding is a planned operation with its own budget. | Changing the embedding model invalidates every stored vector. If that is not planned, the platform is locked to its first embedding model forever. | Never change the embedding model, which is a real strategy and a slowly accumulating quality debt. | A dual-index period during migration doubles the memory cost, and the re-embedding run is a large, spiky compute and token expense. |
Self-hosted small models on GPU capacity
The main reasoning model on a hosted API was fine. The *supporting* models were not: embedding and re-ranking were called two hundred times per run, and at hosted per-call pricing a single run cost more than the customer's monthly subscription. The volume was high, the models were small, and the workload was steady — the three conditions that make self-hosting arithmetic work.
| Decision | Reason | Alternative | Trade-off |
|---|---|---|---|
| Self-host only the small, high-volume models. | A GPU instance is expensive per hour and cheap per request *if it stays busy*. Two hundred embedding calls per run keep it busy; a handful of reasoning calls per run do not. | Keep everything hosted, which has zero operational burden and pays per call forever — the right answer at low volume and the reason to check the arithmetic rather than the fashion. | You take on GPU capacity management, driver and runtime versions, model version rollout, and a scarce instance type that may be unavailable in your region when you need to scale. Utilization below roughly half turns the whole saving into a loss. |
| Batch requests at the inference server. | Accelerators are throughput devices: a batch of thirty-two short requests costs little more than one. Without batching you are renting a GPU to do one thing at a time, which is the most expensive possible way to use it. | One request at a time for the lowest per-request latency, which is correct for interactive single-user inference and wasteful for a pipeline. | Batching adds queueing latency per request, and the batch window is a direct latency-versus-cost dial that must be set with the product's expectations in hand. |
| Treat model weights as a deployment artifact with a slow scale-out path. | Pulling tens of gigabytes of weights dominates instance startup, so GPU capacity cannot be autoscaled reactively the way a stateless API can. Scale-out is minutes, and planning must account for it. | Pull weights on demand per request, which is flexible and makes every cold instance uselessly slow for its first several minutes. | A baked image is large and slow to build, and every model update becomes a full image rebuild and rollout — model versioning is now a deployment problem. |
Per-run tracing and cost attribution
Two failures with the same root cause. A prompt change tripled token usage per run and nobody noticed for nine days, until the invoice arrived. And when a customer reported a wrong answer, there was no way to see which tool call or which retrieved chunk produced it — the run was a black box that had already been discarded.
| Decision | Reason | Alternative | Trade-off |
|---|---|---|---|
| Every run produces a trace: steps, tool calls, retrieved context ids, tokens. | Agent behaviour is non-deterministic, so "reproduce it locally" is not a debugging strategy. The recording made during the run is the only evidence that will ever exist. | Log the final output only, which is compact and makes every quality investigation guesswork. | Traces are large — often larger than all other data the platform produces — so retention is a real cost decision, and they contain customer content, which makes them a data-protection surface with access controls and deletion obligations of their own. |
| Cost is attributed per run and per tenant at the gateway. | On a platform whose marginal cost is tokens, unit economics is an operational metric. Without per-tenant attribution you cannot price the product, cannot detect an unprofitable customer, and cannot tell growth from regression. | Read the provider's monthly invoice, which is accurate, aggregate, and arrives far too late to act on. | Every call must carry tenant and run tags, which is a discipline that decays unless it is enforced at the gateway rather than requested of callers. |
| Alert on tokens per run against a baseline, and on a per-run budget ceiling that aborts the run. | It catches the nine-day prompt regression on day one, and it stops a single looping agent from spending without limit. A hard ceiling is the only defence against a failure mode whose cost is unbounded. | Trust prompt review and a monthly budget, which is how the nine-day regression happened. | A ceiling occasionally kills a legitimate complex run, so it must be visible to the user and adjustable per tenant — and the baseline must be reset deliberately after every intentional prompt change, or it pages on every release. |
What would break this
Every design has a load, a failure or an organization size at which it stops being the right one.
- Runs that must last hours rather than minutes. Queue visibility timeouts and heartbeats stop being adequate, and you need a durable workflow engine with explicit resumable state — a different class of infrastructure.
- Strict data residency or a customer who forbids their data leaving your infrastructure. That forces self-hosted reasoning models and turns the GPU stage from an optimization into the foundation of the platform.
- Tools with irreversible real-world side effects — payments, deletions, external postings. No sandbox fixes that; it requires human approval steps and a two-phase design in the agent loop itself.
- A tenant whose corpus dominates the vector index. Shared-index economics stop working and that customer needs dedicated capacity, which is a pricing conversation before it is an infrastructure one.
- Sustained GPU demand in a region where the instance type is scarce. Capacity, not cost, becomes the constraint, and reserved capacity or a second region becomes the answer.
- Trace volume outgrowing the application it observes. Sampling becomes necessary, and every sampling decision is a decision about which future incident you will be unable to investigate.
Cost shape
Drivers and relative weights. Never a price.
Bars are relative weights, not currency. Real rates depend on provider, region, commitment and volume.