{"id":22597,"date":"2026-07-23T01:23:04","date_gmt":"2026-07-23T01:23:04","guid":{"rendered":"https:\/\/scannn.com\/in-house-llm-serving-at-netflix\/"},"modified":"2026-07-23T01:23:04","modified_gmt":"2026-07-23T01:23:04","slug":"in-house-llm-serving-at-netflix","status":"publish","type":"post","link":"https:\/\/scannn.com\/lv\/in-house-llm-serving-at-netflix\/","title":{"rendered":"In-House LLM Serving at Netflix"},"content":{"rendered":"\n<div>\n<div>\n<div>\n<div class=\"speechify-ignore am kk\">\n<div class=\"speechify-ignore cs e\">\n<div class=\"am kl km kn ko kp kq kr ks kt ku kv\">\n<div class=\"am j kv\">\n<div class=\"am kw\">\n<div>\n<div class=\"bt\">\n<div tabindex=\"-1\" class=\"cq\"><a href=\"https:\/\/netflixtechblog.medium.com\/?source=post_page---byline--a5a8e799ea2c---------------------------------------\" rel=\"noopener follow\"><\/p>\n<div class=\"e kx ky ee kz la\">\n<div class=\"e dm\"><\/div>\n<\/div>\n<p><\/a><\/div>\n<\/div>\n<\/div>\n<\/div>\n<p><span class=\"z b cr u cv\"\/><\/div>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<p id=\"5e4f\" class=\"pw-post-body-paragraph os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm ik cv\"><em class=\"pn\">By AI Platform\u2019s Model Runtime team and Inference team<\/em><\/p>\n<h2 id=\"e501\" class=\"po pp jj z pq pr ps pt fl pu pv pw fn px py pz qa qb qc qd qe qf qg qh qi qj cv\">Introduction<\/h2>\n<p id=\"5e53\" class=\"pw-post-body-paragraph os ot jj ou b ov qk ox oy oz ql pb pc fo qm pe pf fr qn ph pi fu qo pk pl pm ik cv\">Most organizations consume LLMs through hosted APIs. Netflix went further \u2014 we run the full stack ourselves, from model deployment through inference, inside our existing production environment rather than a separate ML silo. Some of those decisions weren\u2019t obvious, and a few revealed their trade-offs only under production load.<\/p>\n<p id=\"f1cd\" class=\"pw-post-body-paragraph os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm ik cv\">This post focuses on the choices where alternatives were seriously considered: engine selection, model packaging, API surface design, deployment strategy, and output constraints enforcement. The goal is to share not just what was built, but why \u2014 and what production revealed that the design phase didn\u2019t anticipate.<\/p>\n<h2 id=\"b9f1\" class=\"po pp jj z pq pr ps pt fl pu pv pw fn px py pz qa qb qc qd qe qf qg qh qi qj cv\">Architecture Overview<\/h2>\n<p id=\"286b\" class=\"pw-post-body-paragraph os ot jj ou b ov qk ox oy oz ql pb pc fo qm pe pf fr qn ph pi fu qo pk pl pm ik cv\">Member-scale ML at Netflix is fronted by a unified JVM-based serving system that handles the end-to-end flow for downstream consumers: routing and A\/B test logic, candidate generation, feature fetching, inference, post-processing, and logging at each stage. Both real-time and cached batch paths are supported. Figure 1 shows the two ways callers reach inference today: the gRPC path through this serving system and a direct HTTP path used by newer LLM-driven applications.<\/p>\n<p id=\"9db1\" class=\"pw-post-body-paragraph os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm ik cv\">Where inference runs depends on the model. Small CPU models run in-process, avoiding remote-call overhead. Larger models need GPUs \u2014 the serving system handles pre- and post-processing locally but delegates inference to a remote service, <strong class=\"ou jk\">Model Scoring Service (MSS)<\/strong>. MSS is the shared inference backend, supporting XGBoost, TensorFlow, PyTorch, and LLMs behind a unified interface, with NVIDIA Triton Inference Server underneath managing model loading, batching, and GPU scheduling.<\/p>\n<p id=\"9176\" class=\"pw-post-body-paragraph os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm ik cv\">On top of Triton sits a Java control plane that handles deployment, versioning, health checking, autoscaling, and multi-region rollout. Model authors package their artifacts and configure the deployment; the control plane provisions GPU instances, configures Triton, and orchestrates zero-downtime upgrades.<\/p>\n<figure class=\"qs qt qu qv qw qx qp qq paragraph-image\">\n<div role=\"button\" tabindex=\"0\" class=\"qy qz dm ra cs rb\"><span class=\"dr ds dt ac du dv dw dd dx speechify-ignore\">Press enter or click to view image in full size<\/span><\/p>\n<div class=\"qp qq qr\"><picture><source srcset=\"https:\/\/miro.medium.com\/v2\/resize:fit:640\/format:webp\/1*GKGOrp0xddZwHomMhiSeCA.png 640w, https:\/\/miro.medium.com\/v2\/resize:fit:720\/format:webp\/1*GKGOrp0xddZwHomMhiSeCA.png 720w, https:\/\/miro.medium.com\/v2\/resize:fit:750\/format:webp\/1*GKGOrp0xddZwHomMhiSeCA.png 750w, https:\/\/miro.medium.com\/v2\/resize:fit:786\/format:webp\/1*GKGOrp0xddZwHomMhiSeCA.png 786w, https:\/\/miro.medium.com\/v2\/resize:fit:828\/format:webp\/1*GKGOrp0xddZwHomMhiSeCA.png 828w, https:\/\/miro.medium.com\/v2\/resize:fit:1100\/format:webp\/1*GKGOrp0xddZwHomMhiSeCA.png 1100w, https:\/\/miro.medium.com\/v2\/resize:fit:1400\/format:webp\/1*GKGOrp0xddZwHomMhiSeCA.png 1400w\" sizes=\"(min-resolution: 4dppx) and (max-width: 700px) 50vw, (-webkit-min-device-pixel-ratio: 4) and (max-width: 700px) 50vw, (min-resolution: 3dppx) and (max-width: 700px) 67vw, (-webkit-min-device-pixel-ratio: 3) and (max-width: 700px) 65vw, (min-resolution: 2.5dppx) and (max-width: 700px) 80vw, (-webkit-min-device-pixel-ratio: 2.5) and (max-width: 700px) 80vw, (min-resolution: 2dppx) and (max-width: 700px) 100vw, (-webkit-min-device-pixel-ratio: 2) and (max-width: 700px) 100vw, 700px\" type=\"image\/webp\"\/><source data-testid=\"og\" srcset=\"https:\/\/miro.medium.com\/v2\/resize:fit:640\/1*GKGOrp0xddZwHomMhiSeCA.png 640w, https:\/\/miro.medium.com\/v2\/resize:fit:720\/1*GKGOrp0xddZwHomMhiSeCA.png 720w, https:\/\/miro.medium.com\/v2\/resize:fit:750\/1*GKGOrp0xddZwHomMhiSeCA.png 750w, https:\/\/miro.medium.com\/v2\/resize:fit:786\/1*GKGOrp0xddZwHomMhiSeCA.png 786w, https:\/\/miro.medium.com\/v2\/resize:fit:828\/1*GKGOrp0xddZwHomMhiSeCA.png 828w, https:\/\/miro.medium.com\/v2\/resize:fit:1100\/1*GKGOrp0xddZwHomMhiSeCA.png 1100w, https:\/\/miro.medium.com\/v2\/resize:fit:1400\/1*GKGOrp0xddZwHomMhiSeCA.png 1400w\" sizes=\"(min-resolution: 4dppx) and (max-width: 700px) 50vw, (-webkit-min-device-pixel-ratio: 4) and (max-width: 700px) 50vw, (min-resolution: 3dppx) and (max-width: 700px) 67vw, (-webkit-min-device-pixel-ratio: 3) and (max-width: 700px) 65vw, (min-resolution: 2.5dppx) and (max-width: 700px) 80vw, (-webkit-min-device-pixel-ratio: 2.5) and (max-width: 700px) 80vw, (min-resolution: 2dppx) and (max-width: 700px) 100vw, (-webkit-min-device-pixel-ratio: 2) and (max-width: 700px) 100vw, 700px\"\/><img fetchpriority=\"high\" alt=\"\" class=\"cs er rc rd\" width=\"700\" height=\"392\" loading=\"eager\" role=\"presentation\"\/><\/picture><\/div>\n<\/div><figcaption class=\"re bv rf qp qq rg rh z b cr u w\">Figure 1. Serving Architecture Overview<\/figcaption><\/figure>\n<h2 id=\"dd34\" class=\"po pp jj z pq pr ps pt fl pu pv pw fn px py pz qa qb qc qd qe qf qg qh qi qj cv\">Design Decisions and Implementation<\/h2>\n<p id=\"f947\" class=\"pw-post-body-paragraph os ot jj ou b ov qk ox oy oz ql pb pc fo qm pe pf fr qn ph pi fu qo pk pl pm ik cv\">Four decisions shape this platform \u2014 engine, packaging, API surface, and rollout \u2014 presented in dependency order, since each one constrains the next.<\/p>\n<h3 id=\"398b\" class=\"ri pp jj z pq fk rj ao fl fm rk aq fn fo rl fp fq fr rm fs ft fu rn fv fw ro cv\">vLLM as the Paved-Path Engine<\/h3>\n<p id=\"954f\" class=\"pw-post-body-paragraph os ot jj ou b ov qk ox oy oz ql pb pc fo qm pe pf fr qn ph pi fu qo pk pl pm ik cv\">The platform was originally built on TensorRT-LLM, a performant inference engine at the time and already integrated with Triton \u2014 the compute backend in use within MSS.<\/p>\n<p id=\"0e48\" class=\"pw-post-body-paragraph os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm ik cv\">By summer 2025, two things had shifted: open-source engines had largely closed the performance gap with specialized stacks, and our workload mix had broadened to include embedding generation, prefill-only inference for ranking and retrieval, autoregressive decoding, and custom models with non-trivial per-step constraint logic. We re-benchmarked against this mix and <strong class=\"ou jk\">selected vLLM as our paved-path engine<\/strong> on operational fit:<\/p>\n<ul class=\"\">\n<li id=\"a052\" class=\"os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm rp rq rr cv\"><strong class=\"ou jk\">Loads custom model architectures without a multi-step compilation pipeline<\/strong> \u2014 faster iteration on non-standard models.<\/li>\n<li id=\"01a5\" class=\"os ot jj ou b ov rs ox oy oz rt pb pc fo ru pe pf fr rv ph pi fu rw pk pl pm rp rq rr cv\"><strong class=\"ou jk\">Extensibility hooks for custom decoding logic<\/strong> \u2014 necessary for the constrained-decoding work described later.<\/li>\n<li id=\"2385\" class=\"os ot jj ou b ov rs ox oy oz rt pb pc fo ru pe pf fr rv ph pi fu rw pk pl pm rp rq rr cv\"><strong class=\"ou jk\">Debuggability<\/strong> \u2014 easier to inspect failures and intermediate state than with a compiled engine in earlier TensorRT-LLM.<\/li>\n<li id=\"c2b9\" class=\"os ot jj ou b ov rs ox oy oz rt pb pc fo ru pe pf fr rv ph pi fu rw pk pl pm rp rq rr cv\"><strong class=\"ou jk\">Familiarity<\/strong> \u2014 many ML practitioners were already using vLLM in research, which cut the research-to-production handoff cost.<\/li>\n<\/ul>\n<h3 id=\"6419\" class=\"ri pp jj z pq fk rj ao fl fm rk aq fn fo rl fp fq fr rm fs ft fu rn fv fw ro cv\">Integrating vLLM into Triton<\/h3>\n<p id=\"b5bc\" class=\"pw-post-body-paragraph os ot jj ou b ov qk ox oy oz ql pb pc fo qm pe pf fr qn ph pi fu qo pk pl pm ik cv\">With vLLM picked, the next decision was how to package models for it. Triton supports two ways, and the choice has significant implications for maintainability \u2014 specifically, how tightly model artifacts are coupled to frontend upgrades.<\/p>\n<ul class=\"\">\n<li id=\"499d\" class=\"os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm rp rq rr cv\"><strong class=\"ou jk\">Python backend.<\/strong> The author defines explicit input\/output tensor specs at packaging time. These specs are frozen in the artifact and must match what the third-party vendor\u2019s frontend\u2019s request builder expects, so every frontend upgrade that touches I\/O specs requires a coordinated change to packaging code; otherwise, requests fail at runtime.<\/li>\n<li id=\"e464\" class=\"os ot jj ou b ov rs ox oy oz rt pb pc fo ru pe pf fr rv ph pi fu rw pk pl pm rp rq rr cv\"><strong class=\"ou jk\">vLLM backend.<\/strong> The artifact is just a JSON config pointing to the model weights and tokenizer. Triton\u2019s vLLM backend reads this config and generates I\/O tensor specs dynamically at deployment time \u2014 the author never defines them. Models and frontend evolve independently.<\/li>\n<\/ul>\n<p id=\"bd3c\" class=\"pw-post-body-paragraph os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm ik cv\">The vLLM backend is the architecturally correct default. Two things bit us in production:<\/p>\n<ul class=\"\">\n<li id=\"264e\" class=\"os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm rp rq rr cv\"><strong class=\"ou jk\">Triton\/vLLM version mismatch.<\/strong> Triton\u2019s vLLM backend is compiled against a specific vLLM API surface. When the two drift \u2014 for example, Triton 25.09 importing vllm.engine.metrics, a module removed in vLLM 0.11.2 \u2014 the backend fails to load entirely. The platform has to pin compatible versions when baking the service image, and prevent model authors from overriding the vLLM version at packaging time.<\/li>\n<li id=\"4740\" class=\"os ot jj ou b ov rs ox oy oz rt pb pc fo ru pe pf fr rv ph pi fu rw pk pl pm rp rq rr cv\"><strong class=\"ou jk\">Custom model logic.<\/strong> The vLLM backend expects a standard HuggingFace-compatible model and handles the full inference lifecycle. Models needing custom preprocessing, postprocessing, or non-standard execution \u2014 ensemble pipelines, custom tokenization \u2014 must use the Python backend, which gives full control over execute(). This escape hatch will likely remain necessary for a subset of models.<\/li>\n<\/ul>\n<h3 id=\"595f\" class=\"ri pp jj z pq fk rj ao fl fm rk aq fn fo rl fp fq fr rm fs ft fu rn fv fw ro cv\">Ecosystem-Compatible HTTP Frontend<\/h3>\n<p id=\"f244\" class=\"pw-post-body-paragraph os ot jj ou b ov qk ox oy oz ql pb pc fo qm pe pf fr qn ph pi fu qo pk pl pm ik cv\">With engine and packaging settled, the next question is how callers reach the system. A key design goal of our system was that LLM models should <strong class=\"ou jk\">NOT<\/strong> be special snowflakes. Every model \u2014 XGBoost ensemble or large-scale LLMs \u2014 is scored via the same gRPC call, so we reuse the same client libraries, health checking, and deployment pipelines. Given that the OpenAI-compatible API interface has become the de facto interface for the LLM ecosystem \u2014 inference engines, orchestration frameworks, evaluation tools, and client libraries all speak it \u2014 so we <strong class=\"ou jk\">expose the OpenAI-compatible API as an additional frontend alongside gRPC<\/strong>.<\/p>\n<p id=\"c236\" class=\"pw-post-body-paragraph os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm ik cv\">The payoff shows up in the experimentation-to-production path: graduating from a hosted model to a fine-tuned self-hosted one \u2014 for quality, latency, cost, or data privacy \u2014 is nearly seamless. Same API, minimal code changes.<\/p>\n<div class=\"rx et ry rz am j gh\">\n<p><h2 class=\"sf b sg sh si sj sk\">Get <!-- -->Netflix Technology Blog<!-- -->\u2019s stories in\u00a0your\u00a0inbox<\/h2>\n<\/p>\n<div class=\"sl e bv\">\n<p class=\"z b cr u w\">Join Medium for free to get updates from\u00a0this\u00a0writer.<\/p>\n<\/div>\n<div class=\"ij e\"><label class=\"am j\"><\/p>\n<div class=\"e\">\n<p class=\"z b cr u cv\">Remember me for faster sign in<\/p>\n<\/div>\n<p><\/label><\/div>\n<\/div>\n<p id=\"630d\" class=\"pw-post-body-paragraph os ot jj ou b ov ox oy oz pb pc fo pe pf fr ph pi fu pk pl rz pm ik cv\">Behind the API, the implementation reuses <a class=\"bx ul\" href=\"https:\/\/docs.nvidia.com\/deeplearning\/triton-inference-server\/user-guide\/docs\/client_guide\/openai_readme.html\" rel=\"noopener ugc nofollow\" target=\"_blank\">NVIDIA\u2019s Triton OpenAI-compatible frontend<\/a>. It starts an embedded Triton server, wraps it in a TritonLLMEngine that converts request schemas into Triton inference requests, and serves responses through FastAPI. KServe HTTP\/gRPC frontends are enabled alongside, so the same Triton instance remains accessible to the Java control plane over gRPC. Adopting Triton\u2019s frontend directly exposed one gap: response_format \u2014 accepted by the schema \u2014 was silently dropped before reaching vLLM, so that a caller requesting JSON output proceeded without guided decoding constraints and could receive malformed JSON with no error surfaced by the platform. We git-subtreed and patched the frontend to translate response_format into vLLM\u2019s guided decoding parameters at request time.<\/p>\n<h3 id=\"243a\" class=\"ri pp jj z pq fk rj ao fl fm rk aq fn fo rl fp fq fr rm fs ft fu rn fv fw ro cv\">Deployment Strategies<\/h3>\n<p id=\"74cd\" class=\"pw-post-body-paragraph os ot jj ou b ov qk ox oy oz ql pb pc fo qm pe pf fr qn ph pi fu qo pk pl pm ik cv\">With API surface and engine in place, the question that remains is how new versions roll out without dropping requests. GPU deployments take longer to bring up than CPU services, and the I\/O schema may change between model versions \u2014 adding a coordination problem on top. The platform offers two strategies:<\/p>\n<ul class=\"\">\n<li id=\"f8c5\" class=\"os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm rp rq rr cv\"><strong class=\"ou jk\">Red-Black<\/strong> deploys a new version alongside the current one. Once the new instance passes health checks, traffic shifts in phases \u2014 the new version scales up while the old scales down at the same rate. If any step fails, the system triggers an atomic rollback. Red-Black is the right choice when the model interface is stable. Production revealed a <strong class=\"ou jk\">coordination gap<\/strong> when a new version requires an I\/O schema change (e.g., new tensor dimensions): the upstream consumer can\u2019t update its config until the new model is fully live, so it inevitably sends \u201cold\u201d requests to a \u201cnew\u201d deployment during the migration window, and those fail.<\/li>\n<li id=\"4508\" class=\"os ot jj ou b ov rs ox oy oz rt pb pc fo ru pe pf fr rv ph pi fu rw pk pl pm rp rq rr cv\"><strong class=\"ou jk\">Versioned<\/strong> solves that gap by maintaining an independent deployment for every (modelId, modelVersion) pair. Multiple versions serve simultaneously, decoupling model deployment from consumer updates: the consumer waits for the new version to be fully ready before switching its config, while the old version keeps serving legacy traffic. The platform cleans up older deployments after inactivity but always preserves the latest. The trade-off is a <strong class=\"ou jk\">temporary increase in GPU cost<\/strong> during the transition overlap.<\/li>\n<\/ul>\n<p id=\"30c7\" class=\"pw-post-body-paragraph os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm ik cv\"><strong class=\"ou jk\">We recommend embedding variable configurations<\/strong> (e.g., tensor shapes) <strong class=\"ou jk\">directly into the inference model<\/strong> to make it version-agnostic, so it can use the cheaper <strong class=\"ou jk\">Red-Black<\/strong> path. <strong class=\"ou jk\">Versioned<\/strong> is reserved for the rare cases where a breaking interface change is unavoidable.<\/p>\n<h2 id=\"1966\" class=\"po pp jj z pq pr ps pt fl pu pv pw fn px py pz qa qb qc qd qe qf qg qh qi qj cv\">Operational Notes<\/h2>\n<p id=\"0bd2\" class=\"pw-post-body-paragraph os ot jj ou b ov qk ox oy oz ql pb pc fo qm pe pf fr qn ph pi fu qo pk pl pm ik cv\">Beyond those four decisions, two operational details are worth flagging \u2014 both hit production gaps the design phase didn\u2019t anticipate.<\/p>\n<h3 id=\"6bbd\" class=\"ri pp jj z pq fk rj ao fl fm rk aq fn fo rl fp fq fr rm fs ft fu rn fv fw ro cv\">Boot sequence<\/h3>\n<p id=\"0ad1\" class=\"pw-post-body-paragraph os ot jj ou b ov qk ox oy oz ql pb pc fo qm pe pf fr qn ph pi fu qo pk pl pm ik cv\">Bringing a vLLM-on-Triton instance up involves several coordinated steps before the gRPC port opens. Two are non-routine.<\/p>\n<ul class=\"\">\n<li id=\"8935\" class=\"os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm rp rq rr cv\"><strong class=\"ou jk\">Model caching.<\/strong> Downloading large LLMs directly from S3 or Hugging Face at startup is slow enough to inflate cold-start latency past what schedulers tolerate. We materialize models on Amazon FSx at the time of model announcement, so warm starts hit a high-performance file system instead of object storage.<\/li>\n<li id=\"073e\" class=\"os ot jj ou b ov rs ox oy oz rt pb pc fo ru pe pf fr rv ph pi fu rw pk pl pm rp rq rr cv\"><strong class=\"ou jk\">Embedded vs standalone Triton.<\/strong> When consumers need the OpenAI-compatible API, Triton runs as an embedded server inside the OpenAI-compatible frontend process; otherwise, it runs standalone. This is configured per-deployment at packaging time.<\/li>\n<\/ul>\n<p id=\"baf4\" class=\"pw-post-body-paragraph os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm ik cv\">The rest of the boot sequence is mechanical: extracting the model package, installing custom vLLM plugins via Python entry_points, cleaning the Prometheus multiprocess directory, and gating the gRPC port until the engine is ready.<\/p>\n<h3 id=\"8033\" class=\"ri pp jj z pq fk rj ao fl fm rk aq fn fo rl fp fq fr rm fs ft fu rn fv fw ro cv\">Unified metrics endpoint<\/h3>\n<p id=\"1e0d\" class=\"pw-post-body-paragraph os ot jj ou b ov qk ox oy oz ql pb pc fo qm pe pf fr qn ph pi fu qo pk pl pm ik cv\">The Prometheus cleanup above hints at a wider observability gap. vLLM writes metrics to PROMETHEUS_MULTIPROC_DIR as .db files; Triton reports server-level metrics through its own Prometheus endpoint. Neither is aware of the other, and Triton\u2019s built-in bridge surfaces only 9 of 40+ vLLM metrics \u2014 missing critical ones like token throughput, KV cache utilization, and prefix cache hit rates.<\/p>\n<p id=\"2801\" class=\"pw-post-body-paragraph os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm ik cv\">We added a lightweight HTTP proxy that merges both into a single \/metrics endpoint: it fetches Triton metrics via HTTP, reads vLLM metrics from disk using Prometheus\u2019s MultiProcessCollector, and returns the combined output. Existing dashboards and alerts work without modification.<\/p>\n<h2 id=\"e3a9\" class=\"po pp jj z pq pr ps pt fl pu pv pw fn px py pz qa qb qc qd qe qf qg qh qi qj cv\">Deep-Dive: Constrained Decoding at Scale<\/h2>\n<p id=\"0f1e\" class=\"pw-post-body-paragraph os ot jj ou b ov qk ox oy oz ql pb pc fo qm pe pf fr qn ph pi fu qo pk pl pm ik cv\">Some Netflix production workloads rely heavily on fine-grained control over token generation. Rather than applying business logic after inference \u2014 paying for invalid generations, then retrying or repairing \u2014 we push constraints inside the decode loop, so the model generates outputs that are compliant by construction. We implement this via vLLM\u2019s custom logits processor interface, modeling each constraint as a state machine that evolves with the generated token history and emits token-eligibility masks at each step. Each request gets its own configured processor, since different requests apply different rules.<\/p>\n<p id=\"a09a\" class=\"pw-post-body-paragraph os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm ik cv\">Getting this to scale ran across two engine versions: we initially deployed on vLLM V0 (V1 had feature gaps), then migrated to V1 in Q4 2025 once it matured. The two subsections that follow are the before-and-after.<\/p>\n<h3 id=\"87db\" class=\"ri pp jj z pq fk rj ao fl fm rk aq fn fo rl fp fq fr rm fs ft fu rn fv fw ro cv\">Why the first implementation didn\u2019t scale<\/h3>\n<p id=\"c810\" class=\"pw-post-body-paragraph os ot jj ou b ov qk ox oy oz ql pb pc fo qm pe pf fr qn ph pi fu qo pk pl pm ik cv\">Our initial pure-Python implementation worked functionally but hit a scaling bottleneck. In vLLM V0, custom logits processors run per-request: the GPU produces logits for the whole batch, the CPU copies them across and waits for the transfer, and then constraint logic runs sequentially for each request \u2014 sequentially because the GIL prevents Python from parallelizing the per-request work. CPU time in logit processing therefore grows linearly with batch size, hitting tail latencies. End-to-end latency becomes CPU-bound even though the model\u2019s forward pass is batched efficiently on GPU. It\u2019s a bottleneck invisible in single-request benchmarks that only surfaces under realistic concurrency. Figure 2 makes the serial pattern visible.<\/p>\n<figure class=\"qs qt qu qv qw qx qp qq paragraph-image\">\n<div role=\"button\" tabindex=\"0\" class=\"qy qz dm ra cs rb\"><span class=\"dr ds dt ac du dv dw dd dx speechify-ignore\">Press enter or click to view image in full size<\/span><\/p>\n<div class=\"qp qq um\"><picture><source srcset=\"https:\/\/miro.medium.com\/v2\/resize:fit:640\/format:webp\/1*KuTzU3O2pm6A4HrUog1c5Q.png 640w, https:\/\/miro.medium.com\/v2\/resize:fit:720\/format:webp\/1*KuTzU3O2pm6A4HrUog1c5Q.png 720w, https:\/\/miro.medium.com\/v2\/resize:fit:750\/format:webp\/1*KuTzU3O2pm6A4HrUog1c5Q.png 750w, https:\/\/miro.medium.com\/v2\/resize:fit:786\/format:webp\/1*KuTzU3O2pm6A4HrUog1c5Q.png 786w, https:\/\/miro.medium.com\/v2\/resize:fit:828\/format:webp\/1*KuTzU3O2pm6A4HrUog1c5Q.png 828w, https:\/\/miro.medium.com\/v2\/resize:fit:1100\/format:webp\/1*KuTzU3O2pm6A4HrUog1c5Q.png 1100w, https:\/\/miro.medium.com\/v2\/resize:fit:1400\/format:webp\/1*KuTzU3O2pm6A4HrUog1c5Q.png 1400w\" sizes=\"auto, (min-resolution: 4dppx) and (max-width: 700px) 50vw, (-webkit-min-device-pixel-ratio: 4) and (max-width: 700px) 50vw, (min-resolution: 3dppx) and (max-width: 700px) 67vw, (-webkit-min-device-pixel-ratio: 3) and (max-width: 700px) 65vw, (min-resolution: 2.5dppx) and (max-width: 700px) 80vw, (-webkit-min-device-pixel-ratio: 2.5) and (max-width: 700px) 80vw, (min-resolution: 2dppx) and (max-width: 700px) 100vw, (-webkit-min-device-pixel-ratio: 2) and (max-width: 700px) 100vw, 700px\" type=\"image\/webp\"\/><source data-testid=\"og\" srcset=\"https:\/\/miro.medium.com\/v2\/resize:fit:640\/1*KuTzU3O2pm6A4HrUog1c5Q.png 640w, https:\/\/miro.medium.com\/v2\/resize:fit:720\/1*KuTzU3O2pm6A4HrUog1c5Q.png 720w, https:\/\/miro.medium.com\/v2\/resize:fit:750\/1*KuTzU3O2pm6A4HrUog1c5Q.png 750w, https:\/\/miro.medium.com\/v2\/resize:fit:786\/1*KuTzU3O2pm6A4HrUog1c5Q.png 786w, https:\/\/miro.medium.com\/v2\/resize:fit:828\/1*KuTzU3O2pm6A4HrUog1c5Q.png 828w, https:\/\/miro.medium.com\/v2\/resize:fit:1100\/1*KuTzU3O2pm6A4HrUog1c5Q.png 1100w, https:\/\/miro.medium.com\/v2\/resize:fit:1400\/1*KuTzU3O2pm6A4HrUog1c5Q.png 1400w\" sizes=\"auto, (min-resolution: 4dppx) and (max-width: 700px) 50vw, (-webkit-min-device-pixel-ratio: 4) and (max-width: 700px) 50vw, (min-resolution: 3dppx) and (max-width: 700px) 67vw, (-webkit-min-device-pixel-ratio: 3) and (max-width: 700px) 65vw, (min-resolution: 2.5dppx) and (max-width: 700px) 80vw, (-webkit-min-device-pixel-ratio: 2.5) and (max-width: 700px) 80vw, (min-resolution: 2dppx) and (max-width: 700px) 100vw, (-webkit-min-device-pixel-ratio: 2) and (max-width: 700px) 100vw, 700px\"\/><img loading=\"lazy\" alt=\"\" class=\"cs er rc rd\" width=\"700\" height=\"194\" loading=\"lazy\" role=\"presentation\"\/><\/picture><\/div>\n<\/div><figcaption class=\"re bv rf qp qq rg rh z b cr u w\">Figure 2: Logits processor serial execution on CPU with vLLM V0<\/figcaption><\/figure>\n<h3 id=\"efd6\" class=\"ri pp jj z pq fk rj ao fl fm rk aq fn fo rl fp fq fr rm fs ft fu rn fv fw ro cv\">vLLM V1 enabled a batch-level design<\/h3>\n<p id=\"ede5\" class=\"pw-post-body-paragraph os ot jj ou b ov qk ox oy oz ql pb pc fo qm pe pf fr qn ph pi fu qo pk pl pm ik cv\">The structural fix arrived in vLLM V1, which moved logits processing to batch level. We rewrote our custom processor to operate on batch-level data structures, computing masks across many requests together, and reimplemented the hot path in C++ with multi-threading to step around the GIL. The V1 API requires explicit tracking of batch membership changes via update_state(batch_update) \u2014 more complex than V0\u2019s per-request interface, but necessary to maintain correct state in a dynamically evolving batch. Figure 3 shows logits processing time staying flat as batch size grows.<\/p>\n<figure class=\"qs qt qu qv qw qx qp qq paragraph-image\">\n<div role=\"button\" tabindex=\"0\" class=\"qy qz dm ra cs rb\"><span class=\"dr ds dt ac du dv dw dd dx speechify-ignore\">Press enter or click to view image in full size<\/span><\/p>\n<div class=\"qp qq un\"><picture><source srcset=\"https:\/\/miro.medium.com\/v2\/resize:fit:640\/format:webp\/1*xqPCzoPySS31VGUnJC0Igg.png 640w, https:\/\/miro.medium.com\/v2\/resize:fit:720\/format:webp\/1*xqPCzoPySS31VGUnJC0Igg.png 720w, https:\/\/miro.medium.com\/v2\/resize:fit:750\/format:webp\/1*xqPCzoPySS31VGUnJC0Igg.png 750w, https:\/\/miro.medium.com\/v2\/resize:fit:786\/format:webp\/1*xqPCzoPySS31VGUnJC0Igg.png 786w, https:\/\/miro.medium.com\/v2\/resize:fit:828\/format:webp\/1*xqPCzoPySS31VGUnJC0Igg.png 828w, https:\/\/miro.medium.com\/v2\/resize:fit:1100\/format:webp\/1*xqPCzoPySS31VGUnJC0Igg.png 1100w, https:\/\/miro.medium.com\/v2\/resize:fit:1400\/format:webp\/1*xqPCzoPySS31VGUnJC0Igg.png 1400w\" sizes=\"auto, (min-resolution: 4dppx) and (max-width: 700px) 50vw, (-webkit-min-device-pixel-ratio: 4) and (max-width: 700px) 50vw, (min-resolution: 3dppx) and (max-width: 700px) 67vw, (-webkit-min-device-pixel-ratio: 3) and (max-width: 700px) 65vw, (min-resolution: 2.5dppx) and (max-width: 700px) 80vw, (-webkit-min-device-pixel-ratio: 2.5) and (max-width: 700px) 80vw, (min-resolution: 2dppx) and (max-width: 700px) 100vw, (-webkit-min-device-pixel-ratio: 2) and (max-width: 700px) 100vw, 700px\" type=\"image\/webp\"\/><source data-testid=\"og\" srcset=\"https:\/\/miro.medium.com\/v2\/resize:fit:640\/1*xqPCzoPySS31VGUnJC0Igg.png 640w, https:\/\/miro.medium.com\/v2\/resize:fit:720\/1*xqPCzoPySS31VGUnJC0Igg.png 720w, https:\/\/miro.medium.com\/v2\/resize:fit:750\/1*xqPCzoPySS31VGUnJC0Igg.png 750w, https:\/\/miro.medium.com\/v2\/resize:fit:786\/1*xqPCzoPySS31VGUnJC0Igg.png 786w, https:\/\/miro.medium.com\/v2\/resize:fit:828\/1*xqPCzoPySS31VGUnJC0Igg.png 828w, https:\/\/miro.medium.com\/v2\/resize:fit:1100\/1*xqPCzoPySS31VGUnJC0Igg.png 1100w, https:\/\/miro.medium.com\/v2\/resize:fit:1400\/1*xqPCzoPySS31VGUnJC0Igg.png 1400w\" sizes=\"auto, (min-resolution: 4dppx) and (max-width: 700px) 50vw, (-webkit-min-device-pixel-ratio: 4) and (max-width: 700px) 50vw, (min-resolution: 3dppx) and (max-width: 700px) 67vw, (-webkit-min-device-pixel-ratio: 3) and (max-width: 700px) 65vw, (min-resolution: 2.5dppx) and (max-width: 700px) 80vw, (-webkit-min-device-pixel-ratio: 2.5) and (max-width: 700px) 80vw, (min-resolution: 2dppx) and (max-width: 700px) 100vw, (-webkit-min-device-pixel-ratio: 2) and (max-width: 700px) 100vw, 700px\"\/><img loading=\"lazy\" alt=\"\" class=\"cs er rc rd\" width=\"700\" height=\"259\" loading=\"lazy\" role=\"presentation\"\/><\/picture><\/div>\n<\/div><figcaption class=\"re bv rf qp qq rg rh z b cr u w\">Figure 3: Batched logits processor execution on CPU with vLLM V1<\/figcaption><\/figure>\n<h3 id=\"ad57\" class=\"ri pp jj z pq fk rj ao fl fm rk aq fn fo rl fp fq fr rm fs ft fu rn fv fw ro cv\">Operational hardening<\/h3>\n<p id=\"f498\" class=\"pw-post-body-paragraph os ot jj ou b ov qk ox oy oz ql pb pc fo qm pe pf fr qn ph pi fu qo pk pl pm ik cv\">Now, performance was no longer the bottleneck. But stateful constraint logic in the decode loop introduced two issues the design phase didn\u2019t anticipate:<\/p>\n<ul class=\"\">\n<li id=\"ad3f\" class=\"os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm rp rq rr cv\"><strong class=\"ou jk\">Partial prefills.<\/strong> V1 performs chunked prefilling, so a request can be prefilled over multiple engine steps. BatchUpdate lacks the granularity to tell whether a request was fully or only partially prefilled, so we added internal tracking.<\/li>\n<li id=\"0024\" class=\"os ot jj ou b ov rs ox oy oz rt pb pc fo ru pe pf fr rv ph pi fu rw pk pl pm rp rq rr cv\"><strong class=\"ou jk\">Preemption.<\/strong> Under memory pressure, vLLM may evict a partially completed request\u2019s KV cache and reschedule it later with a different prompt and output token list. This breaks the state machine\u2019s assumption that the output token list grows monotonically. We detect when the token history shrinks between decode steps, reset the state machine, and reinitialize from the new prompt.<\/li>\n<\/ul>\n<h2 id=\"9e40\" class=\"po pp jj z pq pr ps pt fl pu pv pw fn px py pz qa qb qc qd qe qf qg qh qi qj cv\">Wrap up<\/h2>\n<p id=\"41e1\" class=\"pw-post-body-paragraph os ot jj ou b ov qk ox oy oz ql pb pc fo qm pe pf fr qn ph pi fu qo pk pl pm ik cv\">We set out to build an LLM serving platform for broad production ML requirements \u2014 low latency, deep customization, and integration with existing infrastructure. The result is a system on vLLM and Triton, unified behind a consistent API, designed to give ML practitioners a fast path from experimentation to production.<\/p>\n<p id=\"1b64\" class=\"pw-post-body-paragraph os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm ik cv\">The lessons were often in the details \u2014 version pinning, silent API gaps, packaging trade-offs \u2014 but addressing them has made the platform meaningfully more robust and the developer experience smoother. Next investments reflect where we expect friction:<\/p>\n<ul class=\"\">\n<li id=\"cb82\" class=\"os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm rp rq rr cv\"><strong class=\"ou jk\">System prompt compression<\/strong> to reduce prompt length without sacrificing quality.<\/li>\n<li id=\"a29f\" class=\"os ot jj ou b ov rs ox oy oz rt pb pc fo ru pe pf fr rv ph pi fu rw pk pl pm rp rq rr cv\"><strong class=\"ou jk\">Asynchronous scheduling<\/strong> of vLLM V1.<\/li>\n<li id=\"6cc8\" class=\"os ot jj ou b ov rs ox oy oz rt pb pc fo ru pe pf fr rv ph pi fu rw pk pl pm rp rq rr cv\"><strong class=\"ou jk\">Vectorized logits processors<\/strong> that run as fused GPU kernels instead of CPU code.<\/li>\n<li id=\"a2c4\" class=\"os ot jj ou b ov rs ox oy oz rt pb pc fo ru pe pf fr rv ph pi fu rw pk pl pm rp rq rr cv\"><strong class=\"ou jk\">Lower-precision model variants<\/strong> to decrease memory footprint and increase throughput.<\/li>\n<\/ul>\n<p id=\"b0fe\" class=\"pw-post-body-paragraph os ot jj ou b ov ow ox oy oz pa pb pc fo pd pe pf fr pg ph pi fu pj pk pl pm ik cv\">We\u2019ll continue working closely with the open-source community as this space evolves.<\/p>\n<h2 id=\"3120\" class=\"po pp jj z pq pr ps pt fl pu pv pw fn px py pz qa qb qc qd qe qf qg qh qi qj cv\">Contributions<\/h2>\n<p id=\"27bd\" class=\"pw-post-body-paragraph os ot jj ou b ov qk ox oy oz ql pb pc fo qm pe pf fr qn ph pi fu qo pk pl pm ik cv\">This system is the result of close collaboration and contributions from many teams within the AI Platform org at Netflix. In particular, Liping Peng designed and developed the model packaging workflow and drove the integration of Triton and vLLM with MSS to enable a unified pathway for serving LLMs. Hakan Baba, Nicolas Hortiguera, and ZQ Zhang led GPU capacity planning, system performance tuning, application integration and observability, as well as A\/B test readiness and operational excellence efforts for all production models. Santino Ramos enabled vLLM for production models and optimized constrained decoding performance. Binh Tang developed the initial version of custom model serving and benchmarked different LLM serving frameworks. Lanxi Huang and Daneo Zhang built the serving development tools to enable user self-service. Lingyi Liu drove the overall system architecture and core technical decisions. Abhishek Agrawal and Shaojing Li provide management leadership to ensure alignment, prioritization and execution.<\/p>\n<h2 id=\"6f6a\" class=\"po pp jj z pq pr ps pt fl pu pv pw fn px py pz qa qb qc qd qe qf qg qh qi qj cv\">Acknowledgements<\/h2>\n<p id=\"3897\" class=\"pw-post-body-paragraph os ot jj ou b ov qk ox oy oz ql pb pc fo qm pe pf fr qn ph pi fu qo pk pl pm ik cv\">This work heavily leverages open-source ML libraries, such as Triton, vLLM and PyTorch, etc. We\u2019re especially grateful to the teams and contributors from the community. We also thank our partner teams in Netflix AI for Member Systems for their close collaborations and innovation on the modeling side.<\/p>\n<\/div>\n<p><a href=\"https:\/\/netflixtechblog.com\/in-house-llm-serving-at-netflix-a5a8e799ea2c?utm_source=tldrai\">Source link <\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>By AI Platform\u2019s Model Runtime team and Inference team Introduction Most organizations consume LLMs through hosted APIs. Netflix went further \u2014 we run the full stack ourselves, from model deployment through inference, inside our existing production environment rather than a separate ML silo. Some of those decisions weren\u2019t obvious, and a few revealed their trade-offs [&hellip;]<\/p>\n","protected":false},"author":16,"featured_media":22598,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[143],"tags":[],"class_list":["post-22597","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai"],"_links":{"self":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts\/22597","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/users\/16"}],"replies":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/comments?post=22597"}],"version-history":[{"count":0,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts\/22597\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/media\/22598"}],"wp:attachment":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/media?parent=22597"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/categories?post=22597"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/tags?post=22597"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}