A team ships an on-device inference agent, tests it on the newest phone in the office, sees 60ms end-to-end, and calls the latency target met. Then the p95 across the real fleet lands at 340ms. The code is identical on every device. What changed is everything the code never made explicit: whether WebGPU was available or the runtime fell back to WebGL, whether CoreML picked the Neural Engine or dropped to CPU, how much thermal and memory headroom the device had left after the browser tab count climbed. Those are the real inputs to latency, and none of them were in the agent’s config. This is the gap the “12-factor agent” idea is supposed to close — and the gap most teams miss when they first meet it. The phrase borrows from the Twelve-Factor App methodology, a set of principles for building portable web services: store config in the environment, treat backing services as attached resources, keep dev/prod parity, run processes as disposable. Read casually, “12-factor agent” sounds like a server-side app-config checklist you tick and move on. Applied seriously to client-side ML, it is something sharper: a discipline for making the device’s runtime substrate an explicit, externalized input to your agent, instead of an assumption baked into code that only holds on your test device. What does “12-factor agent” mean in practice for client-side ML? The original twelve factors were written for processes you control end to end — your servers, your container runtime, your load balancer. A client-side ML agent inverts the assumption that made those factors easy. You do not own the runtime. It belongs to whoever bought the phone, opened the browser, or installed the app, and it is different on nearly every one of them. The useful move is to keep the spirit of each factor and reinterpret its subject. “Config in the environment” stops meaning “read database URLs from env vars” and starts meaning “read device capability from a runtime probe before you decide which model and which backend to run.” “Dev/prod parity” stops meaning “run the same Postgres locally and in staging” and starts meaning “your dev device must not be systematically faster than the median production device, or your latency numbers are fiction.” The factors that transfer cleanly are the ones about externalizing environment and disposability; the ones about backing services and logging matter less because a client agent has few backing services and no shared log drain. Here is the reframe that carries the rest of the article: a 12-factor client-side agent treats device capability as a configuration surface it reads at startup, not a constant it was compiled against. WebGL versus WebGPU, CoreML versus an ONNX Runtime CPU path, the amount of HBM or shared memory a mobile SoC exposes, the thermal state — these are environment variables in the twelve-factor sense. Bake them in and they become post-deployment surprises. Read them explicitly and device capability becomes a normal, testable input. Which of the twelve factors actually matter for a client agent? Not all twelve pull equal weight when the “app” is an inference agent running on someone else’s hardware. Sorting them by relevance to client-side ML is more honest than pretending the whole list applies uniformly. Twelve-factor principle Client-side ML reinterpretation Weight for a client agent Config in the environment Read device capability (backend, memory, thermal) at startup; never hardcode the runtime Critical Dev/prod parity Test device profile must match the median fleet device, not the fastest Critical Disposable processes Runtimes are swappable: WebGPU→WebGL, Neural Engine→CPU, without app logic changes Critical Explicit dependencies Declare which backends and model variants the agent may load, per capability tier High Backing services as attached Remote model-serving endpoints are attached resources the agent can detach from and run local Medium Build/release/run separation Model artifacts and quantization variants are release-time choices, not build-time constants Medium Stateless processes Inference state stays per-request; device-cohort state lives in externalized config Medium Logs as event streams Emit per-device latency telemetry so p95 regressions are observable, not inferred Medium Port binding / concurrency / admin / disposability-of-config Mostly server-shaped; minimal direct client analogue Low The three marked critical — config in the environment, dev/prod parity, disposable runtimes — are the ones that decide whether a portable-looking agent actually stays portable. The rest are useful hygiene. If you only internalize three factors for a client agent, internalize those three, because they are the ones that map directly onto the failure mode that ships broken agents. How do you externalize device capability instead of hardcoding it? The concrete mechanism is a capability probe that runs before the agent commits to a model or a backend. On the web, that means checking for navigator.gpu to decide WebGPU versus a WebGL fallback, and reading whatever memory and adapter hints the platform exposes. On Apple hardware, it means letting CoreML report whether the Neural Engine is available rather than assuming it, and having an ONNX Runtime CPU path ready when it is not. The probe produces a small capability descriptor — backend, approximate compute tier, memory headroom, current thermal state — and that descriptor is the config. The agent selects a model variant and an execution provider from it, and the selection logic is data-driven rather than compiled in. This is where the pattern connects to profiling as an engineering practice. A capability descriptor is only as good as the profiling that produced its tiers, which is why we treat the probe and the device profiling used to set those capability tiers as the same workstream, not separate ones. The externalized config is the output of profiling; hardcoding runtime assumptions is what happens when profiling never gets externalized. In our experience across cross-platform inference work, the teams that ship stable p95 latency are the ones that made this descriptor an explicit artifact early, rather than discovering per-device behavior through hotfix cycles after launch — an observed pattern, not a benchmarked figure. The backend choice itself is a portability decision with real consequences. Whether a model even loads the same way across ONNX Runtime and CoreML depends on how the graph was compiled and which operators each provider supports, which is why the compiler flags governing cross-platform ONNX and CoreML inference belong in the same externalized surface. And on the low-level side, whether a WebGL fallback path can reach the GPU at all often comes down to the driver layer — the reason a working OpenCL setup for cross-device inference is part of the same portability contract, not an unrelated detail. What does dev/prod parity mean when “prod” is a fragmented fleet? On a server, dev/prod parity is achievable because you own both ends. On a client fleet, “prod” is thousands of different devices you will never touch, running browser versions and OS builds you did not choose. Strict parity is impossible; the honest version is statistical parity. Your development and CI device profile should sit at or below the median production device, so that a latency number measured in dev is a pessimistic estimate rather than an optimistic one. The failure this prevents is the one that opened this article: a green latency dashboard on the fastest device in the building while the median user misses the target. When the product needs client-side inference to stay inside a sub-200ms interaction budget — the class of target common in real-time media and telecom-adjacent experiences — the difference between the test device’s headroom and the median device’s headroom is the entire margin. Getting this right is why applying 12-factor discipline to telecom-facing media and client-side workloads shows up as fewer per-device hotfix cycles rather than a better score on one device. That is the ROI: deterministic behavior across cohorts cuts the device-specific regression triage that otherwise consumes the weeks after launch. Concretely, parity means your CI matrix runs the agent against a set of capability tiers, not a single golden device. It means p95 latency is reported per-tier and per-cohort, so a regression that only bites the low-memory tier is visible before it ships. Logs-as-event-streams earns its place here: without per-device latency telemetry flowing back, you are inferring fleet behavior from your own laptop, which is exactly the assumption the pattern exists to break. Why do disposable runtimes keep an agent latency-stable? The twelve-factor idea of disposable processes — a process should start fast, shut down cleanly, and be replaceable — maps onto client ML as disposable runtimes. The agent should treat WebGPU, WebGL, the Neural Engine, and a CPU execution provider as interchangeable substrates it can attach to and detach from, with the same model logic on top. When WebGPU is unavailable or the GPU is thermally throttled, the agent falls back to WebGL or CPU without the application layer noticing anything beyond a capability-tier change in its config. This is what keeps latency stable rather than fast on some devices and broken on others. A hardcoded WebGPU-only agent does not degrade gracefully; it fails on every device that lacks the backend, and those failures surface as the late rewrites — retrofitting a WebGL fallback, swapping in a smaller on-device model variant — that inflate a project past its latency-class targets. Disposability means the fallback path was designed in from the start, selected by the capability descriptor, and tested in the same CI matrix as the fast path. The related reasoning about applying deployment discipline to client-side ML inference develops this fallback-as-first-class-path idea further for the deployment side. Where does the 12-factor pattern stop helping? The pattern is a discipline for externalizing runtime assumptions, and its usefulness ends where a shallow capability probe stops being enough. A startup probe can tell you the backend and rough memory headroom. It cannot tell you how a specific model’s attention kernels behave on a specific mobile GPU’s memory hierarchy, or how thermal throttling will unfold over a five-minute sustained session versus a cold-start benchmark. Those questions need a device capability baseline — actual on-device measurement of your model, per representative device class — not just a config surface that reads a few flags. So the boundary is this: 12-factor config discipline is necessary and cheap, and it prevents the most common class of portability failure. It is not sufficient when the workload is heavy enough that per-device kernel behavior and sustained thermal behavior dominate latency. At that point the externalized config should point at a measured baseline rather than a probe’s guess. The profiling-first methodology behind config parity extends past the browser, too — the same discipline of measuring the executor before trusting it applies to server GPU inference environments, which is where the config-parity idea rejoins mainstream inference engineering. FAQ What does working with 12-factor agent involve in practice? It adapts the Twelve-Factor App methodology — config in the environment, dev/prod parity, disposable processes — to ML agents that run on hardware you do not control. In practice it means an agent reads device capability at startup and selects its backend and model variant from that config, rather than being compiled against a single assumed runtime. The result is behavior that stays consistent across a fragmented device fleet instead of only working on the test device. Which of the 12 factors actually matter for a client-side ML agent versus a server app? Three carry most of the weight: config in the environment (read device capability, do not hardcode it), dev/prod parity (test against the median device, not the fastest), and disposable processes reinterpreted as disposable runtimes (WebGPU, WebGL, Neural Engine, and CPU are interchangeable substrates). Explicit dependencies and logs-as-event-streams are useful hygiene. The server-shaped factors — port binding, concurrency, admin processes — have little direct client analogue. How do you externalize device capability as agent config instead of hardcoding runtime assumptions? Run a capability probe before the agent commits to a model or backend — checking for navigator.gpu for WebGPU versus WebGL on the web, or whether CoreML can reach the Neural Engine on Apple hardware. The probe produces a small descriptor (backend, compute tier, memory headroom, thermal state), and that descriptor becomes the config the agent selects from. The selection logic is data-driven rather than compiled in, so device capability is an explicit input rather than a post-deployment surprise. What does dev/prod parity mean when ‘prod’ is a fragmented fleet of user devices? Strict parity is impossible because you never touch the production devices, so the honest target is statistical parity: your dev and CI device profile should sit at or below the median production device, making dev latency a pessimistic estimate. Your CI matrix runs against capability tiers rather than a single golden device, and p95 latency is reported per-tier so a regression that only hits the low-memory cohort is visible before launch. How does treating runtimes as disposable help an agent stay latency-stable across WebGL, WebGPU, and on-device backends? Disposable runtimes mean the agent treats each backend as an interchangeable substrate it can attach to or detach from without changing model logic. When WebGPU is unavailable or the GPU is throttled, it falls back to WebGL or CPU by reading a capability-tier change in its config, so degradation is graceful rather than a hard failure. This designs the fallback path in from the start instead of retrofitting it as a late rewrite that inflates the project past its latency targets. Where does the 12-factor pattern stop helping, and when do you need a full device capability baseline instead? The pattern’s usefulness ends where a shallow startup probe stops being informative — it can report the backend and rough memory headroom, but not how a specific model’s kernels behave on a specific mobile GPU or how thermal throttling unfolds over a sustained session. When the workload is heavy enough that per-device kernel and sustained-thermal behavior dominate latency, you need an actual on-device capability baseline measured per device class. At that point the externalized config should point at a measured baseline rather than a probe’s guess. Config discipline is the cheap, necessary layer that stops the common portability failures; the measured baseline is what you reach for when the workload is heavy enough that the runtime substrate, not the config, becomes the thing you have to prove.