<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Netflix Technology Blog on Medium]]></title>
        <description><![CDATA[Stories by Netflix Technology Blog on Medium]]></description>
        <link>https://medium.com/@netflixtechblog?source=rss-c3aeaf49d8a4------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*BJWRqfSMf9Da9vsXG9EBRQ.jpeg</url>
            <title>Stories by Netflix Technology Blog on Medium</title>
            <link>https://medium.com/@netflixtechblog?source=rss-c3aeaf49d8a4------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 05 Nov 2026 16:08:16 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@netflixtechblog/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Making Failure Diagnosis Reliable with Pensive AI]]></title>
            <link>https://netflixtechblog.com/making-failure-diagnosis-reliable-with-pensive-ai-6b0811a1c015?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/6b0811a1c015</guid>
            <category><![CDATA[pensive]]></category>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[maestros]]></category>
            <category><![CDATA[orchestration]]></category>
            <category><![CDATA[data-science]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Mon, 26 Oct 2026 16:01:02 GMT</pubDate>
            <atom:updated>2026-10-26T17:04:58.324Z</atom:updated>
            <content:encoded><![CDATA[<p>By <a href="https://www.linkedin.com/in/praneethy91/">Praneeth Yenugutala</a>, <a href="https://www.linkedin.com/in/victor-dusa/">Victor Dusa</a>, <a href="https://www.linkedin.com/in/anastasia-samoilova01/">Anastasia Samoilova</a>, <a href="https://www.linkedin.com/in/megan-ren-7b78a81a8/">Megan Ren</a>, <a href="https://www.linkedin.com/in/justinwei96/">Justin Wei</a>, <a href="https://www.linkedin.com/in/mayworm/">Marcelo Mayworm</a></p><p>At Netflix, data and machine learning practitioners run millions of workloads each day across orchestration systems and compute engines such as Titus, Spark, Trino, and Druid. On peak days, roughly 100,000 workloads fail. When workloads fail, the failure itself is only the beginning. Someone still needs to find the useful signal in several logs, decide whether the cause sits in their code or the platform, and determine whether to retry, reconfigure, or escalate. Even a small share of difficult failures can consume engineering time, waste compute, and interrupt critical data pipelines.</p><p>Our first answer to that problem was <a href="https://netflixtechblog.com/auto-diagnosis-and-remediation-in-netflix-data-platform-5bcc52d853d1">Pensive</a>, an auto-diagnosis and remediation system in Netflix’s data platform. Pensive collected logs from failed workloads, used regular expressions and hand-authored Java rules to extract and classify errors, then passed retry or remediation guidance to products such as <a href="https://netflixtechblog.com/maestro-netflixs-workflow-orchestrator-ee13a06f9c78">Maestro</a>. It made ~ 50% of failures self-service and, in some cases, automatic.</p><p>Rules work well for known patterns, but their coverage grows only when someone recognizes a new failure mode, understands the logs, writes a rule, and keeps it current as engines and log formats change. Over one three-month period, Pensive processed roughly seven million job-failure classifications, yet about half fell into the unknown bucket. We needed a way to expand coverage without giving up the deterministic parts of Pensive that already worked.</p><p>To solve this ambiguous and complex problem at Netflix scale, we invested in Pensive AI to add AI-assisted error extractions and focused diagnosis and remediation guidance to our data and ML practitioners. The goal was not simply to place a model in front of a large collection of logs. We needed bounded extraction, predictable evaluation inputs, responsive product behavior, and quality checks that could follow the system from local development through production. Those requirements shaped every layer of the design, which we will discuss in this blog. Pensive AI wraps AI around deterministic processes, which gives broader coverage, low latency, controlled AI costs, and easier maintenance across the software development lifecycle. Building on this foundation, we plan to explore more agentic debugging experiences, as Part 6 describes.</p><p>Early results are encouraging. AI error extraction now covers 100% of failures, including the failures that legacy Pensive classified as unknown. Failed step attempts where no one used Pensive AI averaged 321% more manual debug-log exploration actions than attempts where someone did (an observational comparison, not a causal claim).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2TSVpCdPYj4fMMlFPuheCg.png" /><figcaption><em>Figure 1. Pensive AI architecture. Always-on error extraction and on-demand debugging complement each other and share log sources, the LLM gateway, tracing, and the cache. Archived logs feed offline evals and release gates.</em></figcaption></figure><p>The two paths complement each other. Error extraction improves an existing capability and is always on. It lets the user of a failed Maestro workload quickly check the parts of the logs that may point to an immediate root cause. It has low latency and low cost, and it can help in a large share of cases. On-demand debugging enables a new capability. It looks at symptoms across the logs and adds the LLM’s own knowledge and reasoning for a deeper analysis, at slightly higher latency and cost. Extraction gives a fast first look at every failure, and on-demand debugging triggered by the user goes deeper when that first look is not enough.</p><h3>Part 1: Finding the useful errors</h3><p>Legacy Pensive rules are precise, but each one captures a particular log shape. A simplified rule looks like this:</p><pre>- name: SparkContainerOOM<br>logSource: GENIE_STDERR<br>restartable: false<br>regex: &quot;Container.*is running beyond physical memory limits.*Killing container&quot;<br>summary: Spark AM container runs out of memory</pre><p>With Pensive AI, we wanted to find useful errors that had no corresponding rule and adapt more easily when familiar errors changed shape after a platform upgrade. Data Portal (our UI for data pipelines) surfaces these extracted errors when available and links back to the source logs, giving practitioners a focused starting point for triage. That raises a question: how can we ask a model to identify an error without asking it to repeat a long stack trace token by token?</p><p>Producing output tokens is much slower than reading input tokens. A direct transcription approach would make long errors expensive and slow. Instead, Pensive numbers the lines of each log and gives the model a sequence of {lineNumber, text} records. The model identifies the first and last lines of the error span, and Pensive reconstructs the selected text from its own log copy, leading to lower cost and better latency. This way, the model always returns two integers (start and end line number), whether the selected logs span a few lines or hundreds of lines. The displayed content comes directly from the original log, reducing hallucinations. The model can still choose the wrong range, which is why extraction quality remains an integral measurement for our AI evals, as we will discuss later.</p><p>For orchestrated workflows, “Pensive AI Error Extraction” runs automatically whenever a step fails. A step attempt is one execution of a step within a Maestro workflow. Maestro already calls Pensive for a regex-derived error and restartability classification, so AI extraction runs in the same request and returns its results alongside the established fields. The overall Pensive response must return within Maestro’s hard 90-second post-processing timeout for a failed step attempt.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LwR_-PjFMZiZzmw8QrkyzQ.png" /><figcaption><em>Figure 2. Error extraction. Legacy rules always run. The AI call is bound by a shared deadline, and late calls are excluded. Pensive merges both results into one response and caches it.</em></figcaption></figure><p>That deadline shaped the extraction path. Pensive extracts errors from the logs in parallel to reduce latency, and the per-log calls share one deadline set well inside Maestro’s timeout limit. A per-log extraction call that misses the deadline is excluded from the result, allowing the overall request to return on time.</p><p>The data contract between Pensive AI and the various consumers needed the same care. The legacy data model contained one flattened error string; AI extraction instead returns a list of errors, one for each log source, with deep links back to the originating logs. Changing the old field in place would have broken existing clients. We introduced the new data model beside the legacy non-AI error extraction data model and continued producing both. Data Portal, our user interface for monitoring and observing data pipelines, preferred AI errors when available and fell back to the legacy value when they were not during the rollout. That allowed a gradual rollout and ensured that historical failures still display an error, since we chose not to backfill the old data due to log retention issues.</p><p>The migration is still in progress today. The Data Portal UI surface used for failed Maestro steps reads the new AI extracted error field today, while the query editor (for editing and running SQL), Workbench notebooks (managed compute platform and UI to edit and run jupyter based notebooks) and some slack clients calling Pensive still use the legacy error extraction path. Each client needs a compatible schema change before it can consume the new data. Keeping both fields populated lets teams adopt the new behavior at their own pace without forcing a coordinated cutover as we continue evolving Pensive AI.</p><h3>Part 2: Full failure diagnosis and the product experience</h3><p>Beyond error extraction, we built Pensive AI Debugging, a tool that analyzes failed step attempts and provides root-cause analysis and suggested remediations. It is available through programmatic APIs, MCP, and the product UI (Data Portal), all backed by a common entry point across the Netflix Data Platform. This shared model supports consistent behavior and custom product experiences.</p><p>This diagnosis still has to arrive quickly and remain consistent across investigations. A debugging response can take tens of seconds or a minute to generate, compared with a few seconds for error extraction. We run full AI-based debugging on demand instead of automatically, which keeps costs under control because most failed steps are never opened. We treat the failed step attempt, not the person who triggered it, as the unit of work, so everyone investigating the same failed attempt sees the same diagnosis.</p><p>That choice gives us a stable unit of work for caching. We cache each debugging response by step attempt. A second request becomes a key-value read instead of another AI inference call. The cache also prevents duplicate work, reducing latency and lowering costs further. This also means that a debugging generated in the Data Portal is cached and immediately retrieved for the MCP path instead of incurring another AI inference call. A conditional write creates an in-progress record, one caller starts generation, and other callers wait for the shared result to avoid conflicts when concurrent requests come in for the same step attempt failure. If generation remains in progress past an abandonment timeout, Pensive treats the record as abandoned and reclaims it. Completed responses expire after 90 days to help control costs and optimize storage because debugging requests for much older workloads are uncommon.</p><p>Pensive AI supports multiple ways to initiate debugging responses. A caller can (1) read a cached response, (2) generate one if no cache entry exists, or (3) regenerate and overwrite an existing response. Those semantics support the product UI, programmatic access, and the evaluation harness while keeping the response associated with a single immutable workload failure.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2TdUH-b7FY1G2AvfHVu4QA.png" /><figcaption><em>Figure 3. On-demand debugging. Three client actions share one entry point. A conditional write claims the step attempt. A cache hit returns the cached result. A won claim generates and streams a response, and the result persists even if the client disconnects.</em></figcaption></figure><p>For users, the most important behavior is that a long-running answer feels alive. Pensive streams the response as it is produced. The service emits server-sent events, and the GraphQL layer republishes them as a WebSocket subscription for the Data Portal client.</p><p>Each event carries the complete response generated so far. That duplicates a small amount of text, but it keeps the client simple: it does not need a replay buffer or token reassembly logic, and the next event corrects the display after a dropped or late event. Generation is independent of the connection that requested it. A disconnect does not cancel the work, and the final answer still reaches the cache. A hard reload can show a loading state and poll for the completed result. Within the single-page application backing the Data Portal UI, active streams are maintained independently of view and component lifecycle, so navigation does not interrupt a response in progress.</p><h3>Part 3: Measuring and improving the behavior we want</h3><p>LLM-assisted systems need more than a demo or a few working examples. Before shipping in production, we need confidence in both error extraction and debugging guidance, and we need to know that we have not regressed on failures that legacy Pensive already handled. We built separate evaluation frameworks focused on the two distinct jobs.</p><p>Building labeled data is often the expensive part of evaluation. For error extraction, legacy Pensive’s hand-written rules provide reference labels for failures they recognize. They also serve as excellent ground truth in most cases. We run those rules over production logs and compare their extracted spans with Pensive AI’s output. This gives us a reliable regression set without relabeling every example by hand, though it is not an absolute ground truth (since our human-built rules could also have been stale). For now, we keep these rules frozen as a label source. This lets us continue to build comparison evals and run them on top of the existing system.</p><p>Stable inputs are essential for repeatable evaluation experiments where we A/B test new models/configurations or context injections to improve response quality. The logs Pensive AI uses can expire after as few as seven days or as many as 30 days. Production Pensive AI requests archive fetched logs to S3 object storage asynchronously during workload execution time so the upload is not on the user-facing critical path. Uploads do not overwrite an existing object, preserving the exact snapshot used to create an evaluation case. Evals use those archived logs, so score changes remain attributable to the system under test. The archive also prevents parallel experiments from overloading the source systems and lets us run them without affecting production. Cached S3 log inputs further reduce costs when running evaluations.</p><p>Concretely, we measure four complementary signals for error extraction:</p><ul><li><strong>Inclusion:</strong> An LLM judge compares the legacy Pensive error for a failure with the errors Pensive AI extracted and determines whether the known error is still represented, even when wording differs.</li><li><strong>Precision:</strong> A second judge sees only the extracted AI errors and evaluates whether each is meaningful and actionable on its own.</li><li><strong>Repeated noise:</strong> A script looks across the whole dataset for lines that recur too often. A generic status line may look harmless in one answer but become distracting if it appears in a large share of failures. Lines appearing in more than about 30% of workflows are candidates for explicit exclusion. In practice, the exclusion list has only three lines, each of which otherwise appeared in nearly every extracted error.</li><li><strong>Latency:</strong> We record the latency of every extraction call. Quality scores alone cannot reveal when an apparent improvement requires too much additional work, which often correlates with increased AI costs.</li></ul><p>This combination gives us an actionable feedback loop. Prompt changes can reduce general noise, while the dataset-level view exposes the stubborn boilerplate that prompt changes do not eliminate. Existing rules act as a regression floor, and archived logs give us a repeatable way to compare changes. At Pensive AI’s scale, model and system choices must satisfy quality, latency, maintainability and cost constraints together.</p><p>To seed the debugging ground-truth dataset for evaluation, we mine internal engineering-support Slack threads and use an LLM to identify threads that link to an orchestrated workflow, describe a resolved problem, and match the class of issue Pensive AI handles. A second model synthesizes a reference debugging response from the relevant discussion. Paired with logs from that workflow failure, it becomes a concrete evaluation row in our evals dataset. This approach works only retrospectively, because Pensive AI in production aims to provide debugging guidance before such a Slack discussion happens.</p><p>We also made the debugging generation process visible. Tracing follows the request through Pensive, with child spans for each log fetch and extraction tool call. Our tracing backend based on Braintrust brings the prompt, completion, tool calls, and latency into a single view. During early product development, that visibility helped us inspect low-performing debugging responses in context instead of guessing which input or operation caused them. Tracing helped us improve the responses and fill context gaps even before we had comprehensive evals. In production, the API and UI return the trace identifier with the response, so feedback can lead directly to the trace for in-depth analysis.</p><h3>Part 4: Keeping quality in the release path</h3><p>Evaluation is not only for experiments with LLM orchestration, models, prompts, or context changes. Changes to log selection, trimming logic, or a source system can change what the model sees and therefore change extraction quality. We use the same evaluation harness and evaluators throughout local development and Pensive AI deployment so those effects are visible before users encounter them.</p><p>For each pull request, the code branch is deployed to an isolated preview environment and a CI job runs an experiment with our evals against that isolated environment. Precision and inclusion thresholds gate the merge. The job also runs after a build is promoted, catching interactions that were not present when individual pull requests were evaluated in isolation. If the promoted build misses its thresholds, it alerts the on-call engineer to investigate or roll back.</p><p>The evaluator runs on a schedule against production to surface gradual drift or external changes. Developers can also trigger evaluation experiments remotely while iterating, using the same deployed-environment path as CI. Consistent infrastructure makes the results comparable across local iteration, preview builds, promoted builds, and production.</p><h3>Part 5: Looking for feedback signal from users</h3><p>The system is only valuable if it reduces the amount of manual debugging and log chasing done by our users. We wanted a measurement that was available from product telemetry and close to the behavior Pensive AI was designed to change. Manual debug-log exploration became one of the main signals. The Pensive interface already offers links to logs for a failed step attempt. To estimate whether the feature reduced raw-log hunting, we compared failed step attempts where someone interacted with Pensive AI with those where no one did. The second group averaged 321% more manual debug-log exploration actions. This observational comparison does not establish causality or a specific reduction in mean time to resolution, but it suggests that focused diagnosis often allows practitioners to inspect fewer raw logs. Programmatic MCP access also lets teams build operational flows with their terminals or custom agentic experiences around the same diagnosis service.</p><p>Another more explicit signal we collect is thumbs up and thumbs down on the debugging response. These feedback signals land directly alongside the internal traces so we can quickly associate feedback to the complete log of inputs/outputs and the path the AI took to arrive at the answer. More importantly, we created an internal business-hours alerting system where each thumbs down response sends us a Slack alert. This was valuable to resolve a lot of rough edges and context gaps initially in the product and is helping us improve the product continuously even today.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*b_G74zKbN6phiKnMAcDSzA.png" /><figcaption><em>Figure 4. Feedback loop. A thumbs up or down joins the exact LLM trace that produced the response, by the same logId stamped during generation. Traces and feedback feed ground truth and eval datasets, and each thumbs down sends a Slack alert during business hours.</em></figcaption></figure><h3>Part 6: Future Work building on these foundations</h3><p>Pensive AI is the foundation for more work that goes beyond diagnosis and recommended remediations. We are exploring “Pensive AI Deepthink,” an opt-in managed agentic experience with higher cost and latency. It builds on the precise, low-latency analysis produced by Pensive AI Debugging and gathers additional context from internal documentation, manuals, past Slack discussions, source code, read-only warehouse queries, and Spark execution details. A routing step directs the investigation to the appropriate skill, such as Spark failures, data-audit failures, or late workflows. This keeps the shared starting point stable and deterministic while allowing the investigation to become more specialized, further improving recall. We are also in talks with the Spark team to leverage Pensive AI responses for failure debugging analysis and A/B comparison for upcoming platform migrations.</p><p>We also plan to invest in “Pensive AI Error Classification”. Hard-to-maintain legacy rules also classify failures by restartability and by whether they are platform or user caused. Maestro acts on those labels and changes its retry behavior accordingly like failing fast or tuning the default number of retries (platform errors are tried more than user errors by default). Error classifications must meet a higher bar than error extractions and debugging alone. Moving the majority of the error classifications to AI would let us retire most of the remaining rule-maintenance burden and avoid retrying unknown failures that were unlikely to succeed. For restartability, the costliest error is classifying a recoverable failure as non-restartable, because Maestro will turn a step that could have been recovered into a hard failure, leading to end user alerts and manual remediations. The evaluation framework will need to be tuned around that outcome before AI-enabled classifications can take on those decisions. Equally important, we must avoid retrying non-transient failures that will not succeed, because retries waste compute and delay data pipelines.</p><p>Our work on Pensive AI in Data Platform addresses concrete user pain points in debugging, improves maintainability for platform owners, and improves precision and recall for error extraction. Pensive AI extends coverage to failures that legacy Pensive previously classified as unknown. Reliability comes from the system around the AI model calls: bounded error extractions, compatible client data contracts, responsive delivery, reproducible evaluations, caching and tracing, and release controls that keep quality and cost visible as the platform changes.</p><h3>Part 7: Acknowledgements</h3><p>This work would not have been possible without our stunning colleagues across Data Platform and our Data Engineering and Analytics partners who provided early feedback. While we cannot name everyone individually, we are deeply grateful for their contributions, which shaped both response quality and the product experience. Special thanks to <a href="https://www.linkedin.com/in/adith-swaminathan-98198a21/">Adith Swaminathan</a> for advising on evaluations and inference improvements and acting as a thought partner. Thank you to the Orchestration team members, especially <a href="https://www.linkedin.com/in/rdeepak2002/">Deepak Ramalingam</a> and <a href="https://www.linkedin.com/in/malcolm-ramirez/">Malcolm Ramirez</a> for their additional engineering contributions.</p><p>For dogfooding and integrating Pensive AI as part of user support, we also thank the engineering support organization (ESO Team). We also thank Abhishek Kapatkar (Data Developer Experiences Team) and Emmie Dong (Data Movement Team) for their feedback to improve the Pensive AI integrations with the rest of the ecosystem. Special thanks to the AI Platform team (Kyle Ahn, Rob Mann) and Braintrust Team for helping with distributed tracing, evals infrastructure and model gateway runtime support.</p><p>We also thank Kamran Masood, Ely Spears, Ronald Leung, Anjali Norwood, Prashanth Ramdas, Charles Smith, Harikrishna Menon and other leaders for their constructive feedback and support on the Pensive AI project.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6b0811a1c015" width="1" height="1" alt=""><hr><p><a href="https://netflixtechblog.com/making-failure-diagnosis-reliable-with-pensive-ai-6b0811a1c015">Making Failure Diagnosis Reliable with Pensive AI</a> was originally published in <a href="https://netflixtechblog.com">Netflix TechBlog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Multi‑Ratio Video Resizing for Mobile Feed and Marketing]]></title>
            <link>https://netflixtechblog.com/multi-ratio-video-resizing-for-mobile-feed-and-marketing-2e6b42810f49?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/2e6b42810f49</guid>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Mon, 26 Oct 2026 16:01:01 GMT</pubDate>
            <atom:updated>2026-10-27T00:18:11.960Z</atom:updated>
            <content:encoded><![CDATA[<p><a href="https://www.linkedin.com/in/serdarcellat/">Serdar Cellat</a>, <a href="https://www.linkedin.com/in/yuchen-xie-788a3818/">Yuchen Xie</a>, <a href="https://www.linkedin.com/in/bernadette-marcu-b2980041/">Bernadette Marcu</a>, <a href="https://www.linkedin.com/in/blaidburgess/">Blaid Burgess</a>, <a href="https://www.linkedin.com/in/codymjarrett/">Cody Jarrett</a>, <a href="https://www.linkedin.com/in/cafpj/">Carlos Pereira</a>, <a href="https://www.linkedin.com/in/qing-shi-50a5a349/">Qing Shi</a>, <a href="https://www.linkedin.com/in/samuel-carvajal/">Samuel Carvajal</a>, <a href="https://www.linkedin.com/in/manuel-pamplona-b038a59a/">Manuel Pamplona</a>, <a href="https://www.linkedin.com/in/dorothyjung/">Dorothy Jung</a>, <a href="https://www.linkedin.com/in/feikoritsema/">Feiko Ritsema</a>, <a href="https://www.linkedin.com/in/benjamin-klein-usa/">Benjamin Klein</a>, <a href="https://www.linkedin.com/in/ajwoo/">Alex Woo</a>, <a href="https://www.linkedin.com/in/eugenelok/">Eugene Lok</a>, <a href="https://www.linkedin.com/in/anna-pulido-61025063/">Anna Pulido</a>, <a href="https://www.linkedin.com/in/rebeckaswe/">Rebecka Swe</a>, <a href="https://www.linkedin.com/in/sanford-holsapple/">Sanford Holsapple</a>, <a href="https://www.linkedin.com/in/ranjithkumarshetty/">Ranjith Kumar Shetty</a></p><h3>Introduction</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4BdXCRIW5WXopLnt2qOBmg.jpeg" /><figcaption>Clips in the Netflix mobile app.</figcaption></figure><p>At Netflix, a crucial part of our mission is ensuring our content looks stunning on any device. Vertical formats are increasingly important for Netflix, as we have 1.2B followers across our social media accounts, and <a href="https://about.netflix.com/en/news/introducing-exciting-new-ways-to-find-and-enjoy-your-next-favorite-on-mobile">have recently rolled out Clips</a>, our vertical video feed in the Netflix mobile app. Traditionally, creating vertical assets meant editors and producers manually reframed horizontal videos in editing tools, a process that doesn’t scale as our catalog and use cases grow. To support both our in‑app feed and external marketing campaigns at scale, we built an automated multi‑ratio video cropping system, tightly integrated into our internal production tools, that can generate high‑quality vertical and other aspect‑ratio variants with minimal manual effort.</p><h3>Our Video Resizing Approach</h3><p>Converting cinematic 16:9 footage into mobile‑friendly aspect ratios takes more than a simple center crop. Filmmaking often involves dynamic character blocking and complex camera movement. We need an intelligent “virtual camera” that can follow the key action, respond to how each scene is composed, and produce crops that feel as intentional as the original shot.</p><p>We address this challenge by framing video resizing as a tracking and temporal optimization problem. Our pipeline processes each asset in three main stages, designed to produce 9:16, 4:5, and 1:1 crops in a single pass, without incurring additional computational overhead.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Oh_reVhDMhLVxmfyo-VZaA.png" /><figcaption>An overview of the three-stage process converting horizontal footage into mobile aspect ratios.</figcaption></figure><h4>Shot Detection and Splitting</h4><p>First, we segment the video into atomic shots using a neural‑network–based shot boundary detector. In many cases, shots contain multiple characters conversing on opposite sides of the frame. To better handle these scenarios, we use an Active Speaker Detection based approach to logically split a single continuous shot into multiple sub‑shots. This allows our virtual camera to cut and re-focus on different speakers as the conversation flows. Our Active Speaker Detection leverages an internal face detector and LipFlap to reliably identify active speakers in both live‑action and animated titles.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/1*HFPP2af9zN9i-nJmBp95Cg.gif" /><figcaption>Shot splitting via Active Speaker Detection. The magenta box represents the dynamic vertical crop.</figcaption></figure><h4>Focus Region Discovery</h4><p>Once shots and sub‑shots are defined, we identify the most important region in each frame. We combine face detections with visual saliency maps to filter and prioritize the faces that should be in focus. In scenes with multiple characters, we further rank faces using signals such as active speaker status and relative size. Our model aggregates these cues with shot‑level content understanding to establish a continuous “focus region” for the virtual camera.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rU5iHQ7-E5N1pytKrBaSTw.png" /><figcaption>Blue box shows the focus region around the primary character for reframing.</figcaption></figure><h4>Temporal Reframing</h4><p>Once we’ve established the focus region, the next challenge is deciding how the crop should move over time. Tracking the focus region on a strict frame-by-frame basis would produce a shaky, distracting viewing experience, so we apply a set of heuristics that consider how the focus region moves over the course of a shot. When its displacement is small, we lock a static crop window that consistently covers the relevant area. When there is significant motion, we compute a dynamic tracking window and smooth its path so that any panning feels intentional and cinematic.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/1*yrhOkvafGT5nk0cyHRwaCw.gif" /><figcaption>The vertical crop (magenta box) follows the character’s movement with a smooth panning path.</figcaption></figure><h3>Tooling and Workflow Integration</h3><p>We integrated the resizing algorithm directly into our internal Clips tool, so editors and producers can access it as part of their existing workflow. The interface enables them to automatically generate multiple aspect‑ratio versions of a clip with smart reframing. A unified in‑app module provides intuitive shot‑reframing controls, allowing users to review the algorithm’s crops, and quickly make targeted adjustments where needed.</p><p>For more complex shots or custom campaigns, producers can export the resizing parameters as XML and load them directly into external timeline editors such as Adobe Premiere. This allows agencies and external partners to start from the algorithm’s framing decisions instead of recreating crops manually or relying on tools like Premiere’s Auto Reframe. In our internal test, our resizing algorithm outperforms Auto Reframe on a curated evaluation set, so the XML export effectively brings those higher‑quality crops into external workflows. By bringing automated resizing and manual refinement into a single place, this pipeline significantly reduces production time and manual effort while preserving creative control.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*areusRDQUODlqQAwuTVqTA.png" /><figcaption>Video resizing interface with reframing controls</figcaption></figure><h3>Conclusion</h3><p>Our work on multi-ratio video resizing is one step in a broader effort to make content creation more scalable, without compromising creative intent. By pairing algorithm-driven reframing with editor-friendly tools, we’ve reduced the time teams spend on repetitive resizing tasks and unlocked faster experimentation across mobile feeds and marketing campaigns. As we continue to broaden these capabilities to new formats and workflows, our goal remains the same: to give creatives more time to tell great stories, and to deliver a consistently high-quality viewing experience for our members and fans.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2e6b42810f49" width="1" height="1" alt=""><hr><p><a href="https://netflixtechblog.com/multi-ratio-video-resizing-for-mobile-feed-and-marketing-2e6b42810f49">Multi‑Ratio Video Resizing for Mobile Feed and Marketing</a> was originally published in <a href="https://netflixtechblog.com">Netflix TechBlog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Beyond API Compatibility: Building a Tool to Measure the Blast Radius of Library Changes]]></title>
            <link>https://netflixtechblog.com/beyond-api-compatibility-building-a-tool-to-measure-the-blast-radius-of-library-changes-9474f2fd4f90?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/9474f2fd4f90</guid>
            <category><![CDATA[developer-tools]]></category>
            <category><![CDATA[api-design]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[code-analysis]]></category>
            <category><![CDATA[java]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Fri, 23 Oct 2026 16:01:03 GMT</pubDate>
            <atom:updated>2026-10-23T18:32:52.970Z</atom:updated>
            <content:encoded><![CDATA[<p>By <a href="https://www.linkedin.com/in/robertoperezalcolea/">Roberto Perez Alcolea</a>, Java Platform Team</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*K06WZmnRhhXBboIskWb8qg.png" /><figcaption>Quasar connects a proposed API change with observed references in consumer repositories, giving the producer evidence for a release decision</figcaption></figure><p>At Netflix, shared Java libraries support applications developed across a large, independently evolving repository ecosystem. A library change begins in one repository, but its impact can reach many others. Producers can see the API diff in front of them. What they often cannot see is who still depends on the symbols that changed.</p><p>The need for that visibility grows as coding agents take on more of the software development lifecycle. An agent can accelerate a local edit, but without cross-repository context it cannot judge downstream risk or help plan consumer migrations. The evidence a producer uses to assess a change can also guide the agent working on it.</p><p>Consider a common case: removing a deprecated API. Deprecated entry points expand the surface producers must test, document, and support, and they can constrain how a library evolves.</p><p>Suppose a replacement has been available for several releases and the old method cannot express options required by new behavior. The library’s tests pass, and a compatibility checker flags the removal. The producer still cannot answer the questions that determine what to do next:</p><ul><li>Does any consumer still call this method?</li><li>Are consumers concentrated on one version or spread across several generations?</li><li>Does another library implement or wrap the API, multiplying its impact?</li><li>Is direct coordination sufficient, or does this require a deprecation and migration campaign?</li></ul><p>At small scale, producers can ask nearby teams or search a handful of repositories. Across many repositories, it is harder to find every caller of a method and the library version each one uses. Those answers are scattered across consumer codebases and builds.</p><p>Without that evidence, a producer may release a breaking change and discover the impact through consumer failures, or preserve an old API indefinitely because the risk is unknown. Libraries need to evolve, and their producers need evidence that helps them do so intentionally.</p><p>We built <strong>Quasar</strong> to index symbol definitions and references across Netflix repositories. For library producers, that shared index helps answer two questions:</p><ol><li><strong>What changed in the library’s public API?</strong></li><li><strong>Which repositories use the exact symbols that changed?</strong></li></ol><p>The first identifies a potentially incompatible change. The second estimates its observed blast radius. Together, they turn a generic compatibility warning into evidence a producer can act on before merging or releasing.</p><h3>From API Changes to Consumer Impact</h3><p>Existing techniques each see a different part of the problem. Binary and source compatibility tools identify contract changes, but cannot tell who uses the changed API. Dependency graphs show declared reach, but cannot prove that code calls a particular method. Text search may miss overloads, inheritance, or the artifact version behind a call. Consumer builds provide strong validation, but running them across the fleet is expensive.</p><p>Quasar builds on <a href="https://github.com/sourcegraph/scip">SCIP</a>, an open interchange format for semantic code intelligence. Language-specific indexers encode definitions, references, and relationships using stable symbol identifiers. Quasar retains API definitions for library versions and commits alongside references from consumer repositories. That shared identity lets it connect an API change in one repository to exact usages elsewhere, even when those repositories were indexed independently.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5c7ImyYuNCfO7Tv_ma0EYg.png" /></figure><p>On each enrolled CI build, language-specific indexers submit SCIP indexes to Quasar’s ingestion service. It writes API definitions and consumer references as Parquet in Amazon S3, retaining the symbol identity, dependency version, and source location needed to investigate a change.</p><p>During pull-request review, Quasar compares indexed base and head definitions and counts direct consumer references with Amazon Athena. Quasar exposes those findings through its REST and GraphQL APIs and MCP server; follow-up usage queries provide dependency-version breakdowns and source locations. A separate compatibility-impact assessment can also follow implementations and their callers when a producer supplies a changed JVM symbol.</p><p>Consider this simplified API change:</p><pre>public interface Client {<br>- Result getLegacyValue(Request request);<br>+ Result getValue(Request request, Options options);<br>}</pre><p>For a detailed compatibility-impact assessment of this removal, a producer-side tool supplies the old method descriptor. Quasar constructs its canonical symbol identity. This abridged excerpt from the direct-reference portion of Quasar’s Amazon Athena query shows the essential match:</p><pre>SELECT c.change_id, &#39;DIRECT_REFERENCE&#39; AS evidence_kind,<br>       r.scm_type, r.org, r.repo, r.dep_version, r.file_path<br>FROM requested_changes c<br>JOIN quasar.symbol_refs r<br>  ON r.symbol_canonical = c.target_canonical</pre><p>requested_changes contains the API changes submitted by the producer. The SQL excerpt finds repositories that call the removed method. Quasar also checks implementations and calls through them, so it can count each affected repository once. It returns sample dependency versions and file locations to help investigate the result.</p><p>For the Client change, this tells the producer which repositories still use getLegacyValue, which versions they use, and where some of those calls occur. That helps the producer decide whether to coordinate a migration before removing the method.</p><h3>Bringing the Analysis Before Release</h3><p>Version-to-version comparison is valuable when planning an upgrade or writing a migration guide. Producers benefit even more when feedback arrives during code review.</p><p>For pull-request analysis, Quasar stores API definition snapshots keyed by commit. Given a base and head commit, it classifies symbols as added, removed, or signature-modified. Removals and modifications then become candidates for consumer-impact analysis.</p><p>An incompatible change is not automatically a high-risk change. Quasar reports the observed impact so the producer can decide what to do; it does not enforce a release policy. “No references observed” means only that Quasar found none in the indexed population. Reflection, configuration, dynamic loading, stale branches, or incomplete indexing can hide consumers. The result needs to show whether the requested commits were indexed, how fresh the consumer indexes are, and whether evidence was truncated.</p><p>Pull-request review requires indexed snapshots for the exact base and head commits. If either is unavailable, Quasar reports that it cannot compare them; it does not substitute a default-branch snapshot. This keeps a missing comparison from looking like a clean API diff.</p><p>Return to our Client change. This illustrative report brings together signals available through Quasar&#39;s API comparison, consumer-usage queries, and library-level criticality. It is not a literal response from one API:</p><pre>{<br>  &quot;change&quot;: &quot;REMOVED&quot;,<br>  &quot;symbol&quot;: &quot;Client#getLegacyValue(Request)&quot;,<br>  &quot;affectedRepositories&quot;: 137,<br>  &quot;versionDistribution&quot;: [<br>    { &quot;version&quot;: &quot;4.2.1&quot;, &quot;repositories&quot;: 86 },<br>    { &quot;version&quot;: &quot;4.1.7&quot;, &quot;repositories&quot;: 38 },<br>    { &quot;version&quot;: &quot;3.9.4&quot;, &quot;repositories&quot;: 13 }<br>  ],<br>  &quot;sampleConsumerLocations&quot;: [<br>    {<br>      &quot;repository&quot;: &quot;checkout-service&quot;,<br>      &quot;filePath&quot;: &quot;src/main/java/com/example/checkout/CheckoutClient.java&quot;,<br>      &quot;lines&quot;: [84, 129]<br>    },<br>    {<br>      &quot;repository&quot;: &quot;client-wrapper&quot;,<br>      &quot;filePath&quot;: &quot;src/main/java/com/example/client/ClientAdapter.java&quot;,<br>      &quot;lines&quot;: [57]<br>    }<br>  ],<br>  &quot;libraryCriticality&quot;: {<br>    &quot;tier&quot;: 1,<br>    &quot;basis&quot;: &quot;highest-tier downstream application&quot;<br>  },<br>  &quot;comparison&quot;: {<br>    &quot;baseShaFound&quot;: true,<br>    &quot;headShaFound&quot;: true<br>  }<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QLVdKPyYkqJcNgt0rVN39Q.png" /><figcaption><em>An impact report shows 137 affected repositories distributed across library versions 4.2.1, 4.1.7, and 3.9.4.</em></figcaption></figure><p>The repository count is symbol-specific: in this example, 137 indexed repositories refer to the removed method. The version distribution reveals a dominant cohort on 4.2.1 and a smaller long tail still on 3.9.4. A producer can inspect affected repositories and retrieve indexed file paths, with line numbers where the index provides them. The JSON shows only two sample locations to keep the example readable. The filePath and lines shape mirrors Quasar&#39;s query contract. The tier is broader context about the library, inherited from its highest-criticality downstream application; it does not imply that all 137 callers are Tier 1.</p><p>Together, those signals change the producer’s plan. Instead of merging the removal and waiting for failures, the producer can retain and deprecate the old method, publish a migration path, coordinate a broader migration, and track the long-tail versions before removal. File paths and available line numbers turn that scope into a concrete work list for engineers or coding agents. The same API diff paired with weak or stale evidence would lead to a different action: refresh the indexes before deciding. The value is not merely detecting the removal; it is choosing a proportionate next step.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*o08l-rlQ0D2oo1fBpH5ICw.png" /><figcaption>A pre-release workflow enriches API changes with consumer impact and evidence quality before the producer decides how to proceed</figcaption></figure><h3>A Polyglot Foundation with SCIP</h3><p>The shared <a href="https://github.com/sourcegraph/scip">SCIP</a> format also lets Quasar index several language ecosystems. Each language indexer resolves its own code, while Quasar can use the resulting symbol identities to find references across repositories.</p><p>Conceptually, client-lib indexes Client#getLegacyValue(Request) as a definition, while checkout-service indexes a reference to the same symbol. Quasar can then connect the library API to its consumer across repositories.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RMR331Wf7c4Xx3GTD5_7Rw.png" /><figcaption><em>Language-specific indexers emit SCIP definitions, references, and relationships into Quasar’s shared semantic graph.</em></figcaption></figure><p>Relationships are especially useful when generated code separates the API a producer owns from the symbols consumers invoke. For example, Quasar can trace a Protobuf definition through its generated Java members to the repositories that use them, even when consumer code never names the .proto element.</p><p>SCIP records symbols and their references; it does not determine whether an API change violates an ecosystem’s compatibility rules. Today, Quasar compares indexed JVM and Protobuf API definitions and traces Protobuf changes through generated Java to find affected repositories. A separate JVM compatibility-impact path uses breaks identified by a producer-side tool to query affected repositories. Go, Python, JavaScript/TypeScript, and Rust support usage queries, but Quasar does not yet assess their breaking API changes.</p><h3>Moving Risk Assessment Earlier</h3><p>Today, Quasar surfaces API-change findings on pull requests, including observed consumer counts and confidence. Producers can use its other queries to inspect dependency versions and source locations, with library criticality as broader context. Together, those signals support a decision before merge.</p><p>We want to move that context earlier. An agent writing library code should recognize when it is changing a public symbol and consult Quasar before the change becomes a pull request. Quasar’s APIs and <a href="https://modelcontextprotocol.io/">Model Context Protocol</a> (MCP) integration provide an access path, but access alone does not create awareness. The coding workflow must also recognize public API edits so the agent knows when to query impact and when to involve the producer.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3VqsUQ4rQ3tl5VUPw9ESUQ.png" /><figcaption><em>Today Quasar assesses risk on a pull request. The future workflow gives agents consumer-impact context while they write code, before the pull request is opened.</em></figcaption></figure><p>Earlier guidance will not replace pull-request analysis. The PR assessment will still verify the concrete diff and catch changes that were not recognized during coding. Coverage, freshness, ambiguity, unsupported platforms, and truncation must travel through both checks in machine-readable form. An agent should not turn “no observed usages in the available index” into “safe to remove.”</p><p>Today, agents can query Quasar through MCP when prompted. We are working toward making that context part of the coding workflow, then using identified call sites to prepare migration changes. Quasar supplies semantic evidence, an agent reduces migration toil, and the library producer remains responsible for API and release decisions.</p><h3>What We Learned</h3><p>Building this workflow led us to four practical lessons.</p><ul><li><strong>Separate compatibility from reach.</strong> A removed method can break the API contract even when Quasar finds no callers. A list of affected repositories and their library versions shows the migration work ahead. Producers need both signals to choose a release path.</li><li><strong>Make the evidence inspectable.</strong> A repository count is more useful with sample call sites and a version breakdown. The result also needs to show which commits were indexed, how fresh the consumer indexes are, and whether evidence was truncated. Otherwise, “no usage found” can be mistaken for proof that nobody is affected.</li><li><strong>Keep usage and compatibility separate across languages.</strong> SCIP lets Quasar find symbol references in several ecosystems. Deciding whether an API change is breaking requires compatibility rules or a trusted producer-side signal for that ecosystem.</li><li><strong>Bring evidence to the producer while choices are open.</strong> Pull-request findings can prompt a producer to retain an API, publish a migration path, or coordinate consumers before merge. Moving the same evidence into the coding workflow could shape the edit earlier. After release, it still helps locate consumers that need to migrate.</li></ul><p>Today, Quasar indexes JVM, Go, Python, JavaScript/TypeScript, and Rust code for cross-repository usage analysis. It compares JVM APIs and traces Protobuf changes through generated Java. Producers can review change findings through Quasar’s REST and GraphQL APIs or its MCP server, then investigate affected repositories and call sites. We are extending this foundation toward more ecosystem-specific compatibility checks, deeper code-review integration, and agent-assisted consumer migrations that keep release approval with the producer.</p><p>Quasar is internal to Netflix, but the pattern is portable: index code semantically, retain API definitions, compare them at the point of change, query affected consumers, and report the quality of the evidence. Library producers need more than a compatibility verdict. They need to know who may be affected and have enough time to act before release.</p><p>A huge thank you to the Java Platform team at Netflix. Their expertise, collaboration, and support made this work possible</p><h3>Related Talks</h3><ul><li><a href="https://dpe.org/sessions/roberto-perez-alcolea-aubrey-chipman/from-lag-to-lightning-transforming-dependency-update-timelines/">From Lag to Lightning: Transforming Dependency Update Timelines</a> explores how Netflix shortened dependency-update timelines.</li><li><a href="https://moderne.ai/code-remix-sessions/netflix-journey-automated-code-changes-crs-2026">Netflix’s Journey to Confident Automated Code Changes</a> discusses building confidence in automated changes at scale.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9474f2fd4f90" width="1" height="1" alt=""><hr><p><a href="https://netflixtechblog.com/beyond-api-compatibility-building-a-tool-to-measure-the-blast-radius-of-library-changes-9474f2fd4f90">Beyond API Compatibility: Building a Tool to Measure the Blast Radius of Library Changes</a> was originally published in <a href="https://netflixtechblog.com">Netflix TechBlog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Single-tenant Titus: VM-level isolation on a multi-tenant container platform]]></title>
            <link>https://netflixtechblog.com/single-tenant-titus-vm-level-isolation-on-a-multi-tenant-container-platform-6d8b1909d163?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/6d8b1909d163</guid>
            <category><![CDATA[containers]]></category>
            <category><![CDATA[virtual-machine]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[isolation]]></category>
            <category><![CDATA[compute-platform]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Thu, 22 Oct 2026 22:18:49 GMT</pubDate>
            <atom:updated>2026-10-22T22:19:13.815Z</atom:updated>
            <content:encoded><![CDATA[<p><a href="https://www.linkedin.com/in/samartha/"><em>Samartha Chandrashekar</em></a><em> &amp; </em><a href="https://www.linkedin.com/in/nickbp/"><em>Nick Parker</em></a></p><p>You click play on a Friday night, and in the second that follows, dozens of services answer in milliseconds. Most of them run on Titus, our internal container platform. Titus is, in essence, a Kubernetes-based container hosting platform: we keep both the control plane and the data plane (the container hosts that actually run containers) patched and up to date, so application teams get a consistent and reliable container experience. They hand us a container, and we run it, with no host, cluster, or kernel to own. Titus launches millions of containers every week across tens of thousands of EC2 instances in multiple AWS regions. It is multi-tenant by default: many containers from many teams share each host. That density is what makes container compute efficient, and it is why Titus has been the default home for so much of Netflix’s compute for years.</p><p>For most workloads, sharing a host works well. For a few, it does not, and those few include some of the most demanding real-time systems we run. This post is about them, and about single-tenant Titus, a mode that gives a task its own virtual machine while keeping everything else about Titus the same: the API, the deployment path, and the managed experience where the Compute/Platform team, not the application team, owns the host.</p><p>We will walk through why shared-kernel isolation runs out of road for certain workloads, how we measure isolation rather than asserting it, the options we weighed (microVMs, gVisor, EC2, and more shared-kernel tuning) and why a dedicated VM inside Titus won, how the mode is built on the Titus federation layer and autoscaling, and what we have seen along three axes: isolation, fleet efficiency, and the operational cost of keeping the fleet patched.</p><h3>The cost of good neighbors</h3><p>Multi-tenancy is a tradeoff. Packing many containers onto a large bare-metal instance keeps utilization high, but it also means those containers share a single Linux kernel and the underlying hardware. Titus isolates them with the tools the kernel provides: cgroups cap CPU, memory, and disk; namespaces give each container its own view of the system; seccomp and AppArmor restrict the system calls a container can make; the Completely Fair Scheduler divides CPU time; the iocost controller meters disk IO; and traffic shaping meters each container’s slice of the network. Each container also gets its own identity, IAM role, and security groups, so tenants are isolated for security and resource management.</p><p>These mechanisms provide isolation for what the kernel can partition. They cannot fully partition what is shared in hardware. A group of cores shares a last-level cache and a path to memory, so a container that suddenly pulls a lot of data from RAM evicts a neighbor’s cache lines and slows it down, even when both stay inside their CPU limits. There is no cgroup for memory bandwidth or cache residency, as there is for CPU shares. The interference is real, it is hardware, and it does not show up as either container exceeding its quota.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4IREcZPdQdbU43hA.png" /></figure><p>A noisy neighbor and a co-located app each stay within their cgroup CPU and memory limits, but both draw on a shared last-level cache and memory bandwidth that lack cgroup fences. The noisy neighbor takes most of it, so the co-located app stalls and its tail latency rises even though no one exceeded a quota.</p><p>We have written about this before.<a href="https://netflixtechblog.com/predictive-cpu-isolation-of-containers-at-netflix-91f014d856c7"> Predictive CPU isolation of containers at Netflix</a> moved some placement decisions out of the kernel and into a service that forecasts each container’s CPU demand and re-pins containers to cores to reduce cache interference, formulated as a combinatorial optimization solved every minute.<a href="https://netflixtechblog.com/noisy-neighbor-detection-with-ebpf-64b1f4b3bbdd"> Noisy Neighbor Detection with eBPF</a> instruments the kernel scheduler to surface per-container run-queue latency, so we can see a victim being starved in close to real time. And<a href="https://netflixtechblog.com/mount-mayhem-at-netflix-scaling-containers-on-modern-cpus-f3b09b68beac"> Mount Mayhem</a> traced container-startup slowness to a global kernel lock amplified by modern CPU cache topology. Each of these raised the floor for everyone on the platform, and we are continuing that work.</p><p>A decade of shared-kernel isolation, at Netflix and across the industry, has not made co-tenancy invisible, because a noisy neighbor is not one problem but a family of them. Almost every physical resource a host shares is its own axis of interference, and a cgroup can fence very few of them:</p><ul><li><strong>CPU performance indeterminacy.</strong> Co-located threads contend for the run queue, so scheduling latency rises even while a container remains within its CPU quota. Separately, a neighbor with a large working set evicts a tenant’s<a href="https://www.kernel.org/doc/html/latest/arch/x86/resctrl.html"> last-level cache</a> lines and saturates memory bandwidth, neither of which a CPU share can partition.</li><li><strong>Network bandwidth and packets per second.</strong> Per-container shaping competes for link bandwidth, and receives processing runs in<a href="https://people.kernel.org/dsahern/the-cpu-cost-of-networking-on-a-host"> softirq context</a>, charging one container’s packet work to whatever CPU a neighbor is running on. A flood of small packets exhausts a core’s softirq budget long before any bandwidth cap is reached.</li><li><strong>Storage bandwidth and IOPS.</strong> A neighbor saturating a device’s queue serializes everyone’s I/O, and on cloud block storage, the per-volume and per-instance throughput ceilings are a shared budget in their own right.</li><li><strong>Long tail.</strong> Page-cache reclaim,<a href="https://queue.acm.org/detail.cfm?id=2513149"> NUMA and cross-socket interconnect</a> traffic, TLB-shootdown interrupts, PCIe and GPU contention, package-level power and thermal throttling, and contention on kernel global locks (the class of problem behind<a href="https://netflixtechblog.com/mount-mayhem-at-netflix-scaling-containers-on-modern-cpus-f3b09b68beac"> Mount Mayhem</a>) each degrade neighbors in their own way.</li></ul><p>What unites them is that they are physical, shared, and below the level a quota can reach. A platform can cap how much CPU time or memory a container is granted; it cannot hand each tenant its own slice of the memory bus, its own softirq budget, or its own portion of the last-level cache.</p><p>Of these, the network dimension has bitten hardest in our experience. Per-container bandwidth shaping uses a hierarchical token bucket (HTB) queueing discipline that serializes traffic with a global lock to account for each container’s bytes and packets. Under a flood of small packets from a single container, contention for that lock can burn kernel CPU and starve co-located containers of the time they need to process their own traffic, resulting in TCP retransmits and tail-latency spikes on services that did nothing wrong. The mechanism meant to prevent noisy neighbors becomes one.</p><p>The workloads most exposed to this are stateful, high-throughput streaming jobs: real-time data pipelines and stream processors that continuously move large volumes of data and maintain in-memory state. When one of these is perturbed, recovery is not cheap. A stream processor that falls behind or loses its place may have to restore from a checkpoint and pause processing for many minutes to catch up. For these jobs, intermittent interference from a neighbor is not a small slowdown that averages out; it is a stop-the-world event that pages an on-call engineer. The same teams report, accurately, that they never saw this on a plain EC2 instance.</p><h3>Why we kept coming back to the VM</h3><p>Full VM isolation, where a workload gets its own host and kernel, is the standard our customers are already happy with. The question was not whether VM isolation works; it was how to offer it without giving up what makes Titus worth using. We weighed four paths.</p><ol><li><strong>Keep improving shared-kernel isolation.</strong> This helps every workload on the platform, needs no opt-in, and we are still investing in it (more on the roadmap below). Its limitation is structural. Each round of tuning narrows the gap without closing it, because the underlying hardware remains shared. Cache, memory bandwidth, and the kernel’s own shaping and scheduling locks are not things a container boundary can fully contain. Shared-kernel isolation is the right default and the wrong promise for a workload that cannot tolerate a bad minute.</li><li><a href="https://www.usenix.org/conference/nsdi20/presentation/agache"><strong>microVMs</strong></a><strong> (</strong><a href="https://firecracker-microvm.github.io/"><strong>Firecracker</strong></a><strong>,</strong><a href="https://katacontainers.io/"><strong> Kata Containers</strong></a><strong>).</strong> Lightweight VMs boot in about 150 milliseconds by emulating far less hardware than traditional VMs, which is why they power serverless platforms. They were the most appealing alternative on paper, and we carefully reviewed them. Three things made the cost hard to justify. First, the hardware virtualization they rely on is available only on bare-metal instances or through nested virtualization, and nested virtualization was not ready on the instance types we run at Netflix at the time, so adopting microVMs broadly would have constrained us to a narrow slice of the fleet. Second, there is no managed microVM service we could build on, which means we would have to run and maintain a virtualization layer ourselves: patching the hypervisor, live-migrating guests, and qualifying emulated devices. That is exactly the kind of operational surface we prefer not to own when we can avoid it. Third, even setting those aside, microVMs add virtualization overhead on CPU- and IO-bound work and do not support GPUs. Excellent isolation, but at a price in flexibility and operational load we were not willing to pay.</li><li><a href="https://gvisor.dev/"><strong>gVisor</strong></a><strong>.</strong> A user-space kernel that intercepts system calls provides sandbox-grade isolation without a hypervisor, thereby avoiding the bare-metal constraint. The catch is performance and dependency. Intercepting syscalls in user space adds overhead on IO-heavy paths, which are precisely the workloads we most need to protect, and the open-source project lags behind its internal counterpart in fixes and is effectively maintained by a single vendor. The isolation we would gain came with a throughput tax on the exact jobs we were trying to help.</li><li><strong>Move the workload to EC2.</strong> It is possible to run containers directly on EC2 instances. The problem is that the application team takes back the operational load that Titus exists to carry: choosing instance types, patching hosts, and sizing and scaling the fleet. For a team that came to Titus precisely to avoid those things, this is a regression in the experience, and the move between Titus and EC2 has historically been a one-way door that teams are reluctant to cross.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Df4a-oNs0Y6Di1RV.png" /></figure><p>Spectrum of isolation options from lighter and more shared to stronger isolation: shared-kernel containers, then gVisor, then microVMs (Firecracker or Kata), then a full VM as used by single-tenant Titus, which is highlighted. Each option is annotated with its launch latency, overhead, and the entity that operates it; single-tenant Titus is a dedicated EC2 instance with no additional virtualization overhead.</p><p>There is also a question of what kind of isolation we actually need. microVMs and gVisor are built for security isolation between mutually distrusting tenants, the case where one tenant might actively try to attack or escape into another. Among Netflix teams, that threat model is more than the situation warrants. What these workloads need is resource and performance isolation: a guarantee that a neighbor cannot steal their CPU, cache, memory bandwidth, or network, not protection from a hostile co-tenant. A dedicated EC2 VM delivers exactly that, with hardware we do not have to emulate and a kernel we do not have to sandbox. We pay for the isolation we need rather than for a stronger security boundary.</p><p>microVMs and gVisor asked for a large, bespoke investment with real risk in order to approximate something we could get directly: a VM. Using EC2 to run containers provides the VM, but takes away the managed experience. So we took the direct route and brought single-tenant VM isolation into Titus itself, under the existing API and the existing shared-responsibility model.</p><h3>How single-tenant Titus works</h3><p>The idea is simple to state: a single-tenant task runs on its own EC2 instance, sized to fit it, with nothing else scheduled alongside. That instance is still a Titus container host that the Compute/Platform team owns and patches. The team running the workload sees the same Titus API and the same deployment path. Nothing about how they package or ship the job changes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*O_EERTLEsq8iimoz.png" /></figure><p>Side-by-side anatomy of a multi-tenant Titus, where multiple teams’ containers share one Linux kernel and the hardware beneath it and contend with one another, versus a single-tenant node where one task plus its platform sidecars owns the whole machine with no co-tenants.</p><p>Most of the work happens in the federation layer, the part of Titus that decides where a job runs (which cell, which capacity) before it is scheduled onto a node.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*asJVFsAjpFios_1e.png" /></figure><p>When a job is eligible for single tenancy, three things happen.</p><ol><li><strong>Federation decides eligibility and the envelope.</strong> Eligibility is governed by an allowlist maintained by the Compute/Platform team; a team opts in by targeting a single-tenant workload group rather than flipping a flag in the public API. For an eligible task, federation tags it as single-tenant and records the set of instance families it may run on, ensuring consistent performance across the workload. Consolidating this decision in federation follows the same principle we use for routing in general: placement intelligence belongs in one place, above the individual cells, where it can account for capacity and availability across the region.</li><li><strong>Autoscaling provisions a dedicated node.</strong> Given a pending single-tenant task, our autoscaling implementation launches the smallest instance that satisfies the task’s requested CPU, memory, and GPU from the allowed families for the task just in time. Taking the tightest fit keeps the unused remainder of each dedicated node small. The next section covers this in detail.</li><li><strong>The scheduler enforces one task per node.</strong> Single-tenant pods are configured with pod anti-affinity to prevent colocation with other pods, along with a priority class that exempts the task from preemption. Single-tenant nodes are also tainted to prevent unrelated workloads from being placed there, with only single-tenant workloads tolerating this taint. We also disable defragmentation/repacking for these nodes, so a single-tenant task is never relocated to repack the fleet. Together, these mean a single-tenant task has its host to itself for its entire life, undisturbed by a neighbor or by the platform’s own bin-packing.</li></ol><p>Two design choices are worth dwelling on. First, single tenancy is allowlisted on purpose. If it were a free checkbox in the API, most teams would tick it, because who wouldn’t want a quieter host? And the efficiency that multi-tenancy exists to provide would erode across the fleet. Keeping the Compute/Platform team in control lets us direct dedicated isolation to workloads that need it and steer the rest toward shared capacity, while keeping the public API surface small as we learn. Second, we do not silently resize anyone’s job to fill its node. The job keeps the resources it requested; we recommend sizing it close to a node shape so the dedicated instance is well utilized, while keeping the workload portable and running identically whether in single-tenant mode or not, regardless of the host’s dimensions. Moving in and out of the mode is a configuration change, not a rewrite.</p><h3>A few perpetrators, a lot of collateral damage</h3><p>Noisy-neighbor pain is not spread evenly. In practice, a small number of workloads cause most of the disruption: a job that floods the host with small packets, churns the page cache, or saturates memory bandwidth degrades the performance of every other container unlucky enough to share its host. Some of these are also among the workloads most affected by contention, because a job can be both a source of interference and sensitive to it. The shape of the problem is a few perpetrators and many victims.</p><p>That shape is what makes single tenancy a precise lever rather than a blunt one. We do not need to give every workload its own machine. We need to pull the handful of perpetrators and the most disruption-sensitive victims onto dedicated nodes. Doing so protects their former neighbors directly and removes the reason the platform was spreading sensitive workloads apart with anti-affinity placement in the first place. The hosts left behind are quieter, and because they no longer have to be kept partly empty to honor spread rules, they pack more densely.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*aSioSdH7c103oF16.png" /></figure><p>Before-and-after view of the Titus fleet. Before: a few red perpetrators are scattered across shared hosts, the amber workloads beside them are disrupted, and anti-affinity spreading leaves dashed stranded capacity. After: each perpetrator runs on its own dedicated single-tenant node, the remaining shared hosts are fully loaded with healthy workloads, and one host’s worth of capacity is reclaimed.</p><p>On the left is the fleet before: perpetrators sit on shared hosts, the workloads next to them suffer, and anti-affinity rules leave gaps that cannot be filled. On the right is the same fleet after: each perpetrator has its own right-sized node where it can be as noisy as it likes, with no one to disturb; the remaining hosts run full of undisturbed workloads; and the capacity that used to be stranded comes back. A few dedicated nodes buy isolation for the perpetrators and a calmer, denser fleet for everyone else.</p><h3>Provisioning many small nodes</h3><p>Single tenancy trades fewer large, shared instances for more small, dedicated ones, and that shift has consequences across the stack.</p><p>The first is the control-plane load. More nodes mean more for the Titus control plane to track and more VM launches against the EC2 control plane, the latter mattering most during a regional failover, when we scale up the fastest and can least afford a throttle. Titus is already a federated system: each region is divided into independent cells, which limit how large any single control plane can get, and let us add capacity by adding cells rather than growing a single stack without limit. We migrate workloads in stages rather than all at once, and we exercise the launch-rate path under controlled load on a schedule rather than discovering its limits during an incident.</p><p>The second consequence is capacity management, where the design changed the most. With one task per node, the fleet now spans many instance shapes whose demand shifts continuously as jobs come and go. Pre-staging static pools of every shape would be slow to manage and wasteful, so we instead provision single-tenant nodes on demand. It handles provisioning end-to-end, and several of its features line up with exactly what single tenancy needs:</p><ul><li><strong>Just-in-time, right-sized provisioning.</strong> Single-tenant jobs are configured with a list of supported instance families based on their performance requirements. At launch, they are mapped to the best-fit node size by comparing requested resources against the selected instance families. New nodes of that instance type are brought up by the autoscaler to meet the pending demand. Single-tenant pods are configured with anti-affinity to all other pods, so the scheduler naturally lands one pod per autoscaled node. Newly vacated nodes may be reused by any pending deploys before they’re automatically torn down. The node pools themselves are shared across single-tenant workloads and describe a catalog of available instance families and sizes.</li><li><strong>Single tenant node pools.</strong> Single tenant may run in separate clusters or be shared with multitenant clusters. The pools are configured with a list of supported instance-type permutations and are pinned to a Titus container host AMI. This is where the managed experience lives: the host image and its configuration are ours, declared once and applied to every node created.</li><li><strong>Consolidation management.</strong> In multi-tenant environments, we default to a repack/defrag behavior that periodically relocates workloads. This doesn’t apply in single tenant, where nodes are explicitly each running at most one workload. We therefore reclaim only nodes vacated by workloads that have scaled in or terminated. Node termination is performed in two stages, allowing nodes to be reused for pending workloads before they are fully terminated.</li><li><strong>Drift-based patching.</strong> When we publish a new container host AMI, we support a “mutable” deploy to existing nodes without affecting any hosted containers, and an “immutable” deploy that updates the target AMI for newly created nodes. This means that many node upgrades don’t require disrupting workloads, but we reserve the right to roll the fleet with our users if needed.</li></ul><h3>The launch-latency tax, and why warm pools are harder here</h3><p>Provisioning on demand does not make a node appear instantly, and launch latency is the tradeoff single tenancy asks us to manage most carefully. Bringing a single-tenant task to the point where it can serve traffic has two parts: standing the node up (booting a fresh EC2 instance, then pulling and extracting the container image, which is the largest part of host setup for most jobs) and the application’s own startup (a JVM loading and JIT-compiling its classes, a cache filling, and so on). A fresh instance takes tens of seconds before the image pull even begins.</p><p>In multi-tenant Titus, most of this is hidden. A new task usually lands on a Titus container host that is already running and already has the image layers cached, so starting it takes only a few seconds rather than minutes; the host boot was paid for once, long ago, and amortized across every task that has run there since. Single tenancy gives up that hiding place. Every single-tenant task waits for its own machine to boot and pull its image.</p><p>The usual remedy is a warm pool: keep some instances booted and idle so a pending task lands on one immediately. This works well for the multi-tenant fleet precisely because that fleet is close to homogeneous. A handful of large shapes cover almost everything, since any task can be packed onto a big shared host. A small amount of warm capacity in a few shapes gives a high hit rate, and its cost is spread across the many tasks that will use it.</p><p>Single tenancy inverts those economics, and this is the part worth being explicit about.</p><p>Press enter or click to view image in full size</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*8c3IR8qXRUz7tQ0_.png" /></figure><p>Because every task gets a right-sized node, the fleet spans many instance shapes (several families across many sizes), and a warm instance only helps a pending task whose shape matches it. A warm r7i.2xlarge does nothing for a task that needs an m7a.8xlarge. To achieve a good hit rate, we would need to hold idle capacity in every family-and-size bucket, and idle capacity in a bucket with no task to fill is pure cost. The very property that makes single tenancy efficient at steady state, tight best-fit sizing, works against warm-pool reuse: more distinct shapes mean more buckets and a lower hit rate per dollar of standing capacity. And the pressure peaks exactly when it is least welcome. During a regional failover or a live event, when many tasks need fresh nodes at once, a thin warm pool spread across many shapes drains immediately, and the rest of the demand pays the full cold-boot cost.</p><p>So we treat warm capacity as a small, targeted lever rather than a default. We keep a little pre-warmed capacity for the most latency-sensitive single-tenant starts, and we deliberately narrow the menu of shapes to a curated set of sizes rather than the full continuum, so warm capacity concentrates into fewer buckets and trades a little best-fit efficiency for a better hit rate. Everything else provisions just-in-time and accepts the boot.</p><p>The more durable wins are in cutting the launch cost itself rather than pre-paying for it, and they help both single- and multi-tenant nodes, which is the better place to spend the effort:</p><ul><li><strong>Faster image distribution.</strong> The image pull dominates the host setup, so newer<a href="https://github.com/opencontainers/image-spec"> OCI image formats</a> and lazy-pulling snapshotters such as<a href="https://github.com/containerd/stargz-snapshotter"> stargz</a> and<a href="https://github.com/awslabs/soci-snapshotter"> SOCI</a>, which fetch only the bytes a container actually reads at startup rather than the whole image up front, reduce the largest component of the gap.</li><li><strong>Checkpoint and restore.</strong> For processes that are slow to initialize, snapshotting a warmed-up application with a tool like<a href="https://criu.org/"> CRIU</a> and restoring it, rather than cold-starting it, removes most of the second half of launch latency.</li><li><strong>Instance type selection.</strong> In multitenant configurations, we have historically used metal instances, which offer slightly better performance but come with a slower startup time. In a single-tenant configuration, node startup latency is more important, so we have moved away from these instances.</li><li><strong>Scheduling optimization.</strong> Through regular stress testing of fleet scaling, we found a scheduling bottleneck caused by Kubernetes preemption logic, leading to nodes being scaled up quickly but then left idle while waiting for workloads to be scheduled on them. This was ultimately unnecessary, as our single-tenant pools do not run preemptible workloads. Disabling preemption greatly improved scheduling performance when under strain.</li></ul><p>None of this makes the tradeoff disappear, and it is worth stating plainly: a single-tenant node is slower to stand up than a slot on an already-warm shared host, and hiding that with warm pools costs more per node here than it does on the multi-tenant fleet, because the warm pool cannot be as homogeneous. But for the long-running services that single tenancy targets, paying that cost once at launch in exchange for a quiet life afterward is a trade these teams are glad to make.</p><h3>Measuring the difference</h3><p>Isolation claims are easy to assert and hard to trust, so we measure them with a repeatable benchmark and a single score.</p><p>The method is a differential one. We run a reference container, a healthy and representative application, and record its throughput and tail latency while it runs alone. Then we run it again next to a havoc container engineered to be a bad neighbor: a UDP flood through XDP to stress the network-shaping path, pointer-chasing and STREAM to pollute the last-level cache and saturate memory bandwidth, fsync storms against the disk, stress-ng to abuse the scheduler, and GPU-burn to monopolize an accelerator. We only trust a run once the host has reached a steady state, which we confirm with the kernel’s Pressure Stall Information: we wait until cpu_some or mem_some shows that tasks are actually stalling on a contended resource, not merely that the resource is busy. Each scenario is run many times with fixed seeds, and we report the median with a bootstrapped confidence interval, because noisy-neighbor distributions are heavy-tailed and a simple average can mask the spikes that matter.</p><p>From the reference container’s numbers, we derive a simple measure of how well isolation held. Throughput efficiency is the fraction of operations per second the app retains under attack; latency efficiency is the ratio of its undisturbed P99 to its P99 under pressure. A neighbor that doubles the reference app’s P99 drops the latency efficiency. As an illustration, a reference app whose P99 latency goes down next to a havoc container achieves a certain latency efficiency, and one whose throughput falls from a to b operations per second achieves a certain throughput efficiency. The composite score is a weighted blend of the two, with a latency-sensitive preset that gives greater weight to tail latency for interactive and streaming-path services. A perfectly isolated system scores 100%, and a single hard failure invalidates the run regardless of the average.</p><h3>Noisy-neighbor isolation</h3><p>cgroups container isolation isn’t perfect, and applications may experience worse performance due to sharing a machine with a determined neighbor. This could extend to disk and/or network IO where the same volumes or interfaces may be shared across workloads. For a distributed workload, this can lead to measurable inconsistencies in performance depending on where the replicas are placed. For more sensitive workloads, this variance can mean the difference between stability and getting paged.</p><p>The single-tenant option makes it possible to better isolate workloads that cause problems for their neighbors and/or are more sensitive to such problems. This can be used as a stopgap until the specific regression problem is resolved and consistency is restored, or kept permanent for load patterns that aren’t worth supporting in a shared environment.</p><h3>Efficiency</h3><p>The second effect is fleet efficiency, and it is the one people expect to go the wrong way. Dedicating a node to a single task consumes that node’s slack, so the naive expectation is that utilization falls. In aggregate, it can move the other way, for a reason related to how we dealt with poor isolation before single tenancy existed.</p><p>To protect sensitive workloads on shared hosts, the platform used anti-affinity placement, scattering them across the fleet to reduce colocation of the same workload on the same nodes. Spreading fights bin-packing: it strains capacity, because a host kept partly empty to honor an anti-affinity rule cannot be filled by a workload that would otherwise have fit there. When isolation-sensitive workloads move to dedicated nodes, the need for spreading and fragmentation goes away, and the remaining multi-tenant fleet can pack more densely.</p><p>Another source of fragmentation is services that are sensitive to disruption. These are typically stateful services where recovery is expensive. As these services become more common in a given population, they can lead to nodes being held up by pods with strict disruption requirements. This can lead to an inability to defrag and recompact the pool during off-peak times. The symptoms and resulting drop in fleet efficiency caused by disruption-sensitive workloads mirror what’s observed for workloads with anti-affinity rules.</p><p>Depending on the magnitude of the anti-affinity and/or disruption budget problem, moving such workloads to single-tenant pools can result in equivalent or even improved placement when all costs have been accounted for. It also becomes easier to quantify the cost to end-users. Rather than trying to attribute fleet-wide fragmentation to individual workloads, we can directly quantify waste as single-tenant node overhead.</p><p>The theory and the practice diverge here, and it was the practice that decided it. In principle, the efficiency ceiling is higher for multi-tenant bin-packing: pack enough tasks together, and a host fills almost completely. We do not reach that ceiling. AWS offers a discrete set of instance sizes, so a large shared host rarely matches the exact sum of the requests we place on it, and the gap between a host’s capacity and what we can actually pack onto it is a bin-packing hole we cannot fill. At the granularity of large instances, those holes are sizable. Right-sizing an instance to a single workload, picking the closest fit from many shapes instead of draping many workloads over one big host, captured more of the available efficiency in our experience than the higher theoretical ceiling ever delivered. The clean theory favored multi-tenancy; our measured fleet favored right-sizing.</p><h3>Container-host maintenance</h3><p>The third effect is operational, and it shows up in how we keep the fleet patched. A multi-tenant host is hard to take down for maintenance precisely because it is full: draining it means relocating many tenants at once, several of which carry disruption budgets that limit how fast they can move, so a routine patch becomes a careful, coordinated evacuation. A single-tenant host runs exactly one task, with consolidation and defragmentation turned off, so draining it means relocating only the one workload, and we can automatically handle that when a new AMI ships. The remaining multi-tenant hosts are easier to maintain, too, because the workloads that were most sensitive to disruption are no longer stranded on them.</p><h3>What it changes, and what it doesn’t</h3><p>Single-tenant mode is a configuration, not a migration. A team moves a workload onto a dedicated node, and back, with a labeling change, without rewriting anything and without leaving Titus. That turns the isolation decision into a reversible setting, which is the opposite of the one-way door between Titus and EC2 that teams used to face. A team can try single tenancy for a quarter, measure it, and step back if the trade-off isn’t worth it for them.</p><p>What does not change is the contract. The Compute/Platform team still selects the instance type, patches the host, and sizes and scales the fleet underneath. The application team still uses the Titus API, the same delivery pipeline, and the same observability. Single tenancy is a new point on the isolation axis offered within the existing managed experience, not a separate product that the team has to learn and operate.</p><h3>When a dedicated node is the wrong call</h3><p>A dedicated node is a real cost, and single tenancy is not free isolation. Two things are worth weighing before reaching for it.</p><p>First, a single-tenant task does not get the whole machine; it gets the machine minus overhead. Every Titus container host, dedicated or shared, runs the platform sidecars, system services, the kubelet, and the host environment itself, and that overhead is a roughly fixed slice of CPU and a few gigabytes of memory. On a shared host, that slice is paid once and amortized across the many tasks running there. On a dedicated node, a single task handles it. Because the cost is fixed rather than proportional, it is punishing as a fraction of a small instance and negligible on a large one: a small dedicated node can lose tens of percent of its resources to overhead, while on the largest shapes it rounds to a couple of percent.</p><p>Second, because a task is placed on the smallest instance that fits it, there is usually a gap between what the task asked for and the next available shape, and on a dedicated node, that gap is pure waste, with no neighbors to soak it up. The two effects compound: fixed overhead plus the best-fit remainder set a ceiling on how much of a dedicated node a workload can actually use, and that ceiling is lowest exactly where people are tempted to reach for isolation casually, on small instances. However, this sizing waste can be reduced through coordination with the service owner as needed.</p><p>The guidance follows from the arithmetic. Single tenancy pays off for large, long-running, performance-sensitive, disruption-sensitive workloads, sized close to a node shape and run as fewer, larger replicas rather than many small ones. It is the wrong call for small, bursty, or highly elastic workloads, where the overhead fraction is high, the dedicated node sits mostly idle, and multi-tenant packing is both cheaper and faster to scale. This is also why the mode is allowlisted rather than a self-serve checkbox: the platform can keep workloads on the side of that line where they belong, and leave the ones that would gain little from a node of their own on the shared fleet, where they are cheaper to run.</p><h3>Who’s using it</h3><p>The first adopters are the workloads that motivated the work: real-time data infrastructure. Managed<a href="https://flink.apache.org/"> Apache Flink</a> stream processing, streaming data connectors, and telemetry and logging pipelines were both the most frequent sources of network contention and the most damaged by it, because their recovery is expensive and their disruption budgets are tight. These are stateful, high-throughput jobs where a few seconds of starvation can cascade into minutes of catch-up, and where the platform’s own repacking used to be a source of disruption in its own right.</p><p>Moving them onto dedicated nodes removed the class of problem rather than mitigating it. The teams running them report stable operation, having previously seen intermittent, hard-to-attribute interference, and their jobs are no longer relocated by fleet repacking, removing a second, self-inflicted source of disruption. Latency-sensitive serving paths and a handful of GraphQL and edge services have followed for the same reason: a predictable tail matters more to them than a few points of bin-packing efficiency.</p><p>This is an ongoing rollout, not a finished one. We are widening the set of eligible workloads as we learn, using the largest production jobs as the stress test for the platform’s scaling assumptions, and watching the control-plane and EC2 launch-rate metrics as the count of small nodes grows. We have also been running periodic stress tests to exercise scaling ahead of real user workload, and making improvements to autoscaling and scheduling logic along the way.</p><h3>What’s next</h3><p>We are not done on either side of the tradeoff. On single tenancy, we are reducing launch latency with smarter pre-warming of common shapes, broadening the allowlist as scaling data holds up, and extending the model beyond long-running services as cost attribution and capacity stories mature. On the multi-tenant side, which remains the right default for most workloads, we are addressing the dimensions that hurt the most. The largest is the network: replacing HTB’s global-lock shaping with an<a href="https://ebpf.io/"> eBPF</a>-based<a href="https://docs.cilium.io/en/stable/network/kubernetes/bandwidth-manager/"> Expected Departure Time</a> approach that rate-limits by timestamping packets rather than contending on a lock, and pairing it with a lower-overhead container network device. We are also evaluating hardware cache and memory-bandwidth partitioning (<a href="https://www.intel.com/content/www/us/en/architecture-and-technology/resource-director-technology.html">Intel RDT</a> and AMD PQoS) to give the kernel a real lever on the last-level cache, hardware GPU partitioning (<a href="https://www.nvidia.com/en-us/technologies/multi-instance-gpu/">NVIDIA MIG</a>) for accelerator isolation, and continued work on the cgroup v2 IO controller for disk. This isolation benchmark lets us rank these by return on effort rather than by intuition, and show customers, in the numbers, that the shared fleet is getting quieter over time.</p><p>Single-tenant Titus is also a step toward something larger. Netflix’s Platform teams offer two ways to get compute: virtual machines on EC2 &amp; containers on Titus. We want to replace that with a single Compute API that takes intent rather than mechanism into account: how much isolation a workload needs, how quickly it must launch, how long it can run without disruption, and what business domain it belongs to. The platform then makes placement and runtime choices, and can make globally optimal ones because it sees the whole picture.</p><h3>Conclusion</h3><p>Multi-tenancy is the right default for a container platform, and we will keep making it better for everyone. But the right default is not right for everything. Single-tenant Titus gives the workloads that need it the isolation of a dedicated VM, provisioned and patched by our existing node management, under the same API and the same managed experience, without taking away the freedom to change their mind. We can now point to the difference rather than describe it: a tail that stops moving under a noisy neighbor, a fleet that packs more densely once spreading is retired, and hosts that are faster and safer to patch. For the streaming and data systems at the heart of Netflix’s real-time platform, a room of one’s own turned out to be the feature that mattered.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6d8b1909d163" width="1" height="1" alt=""><hr><p><a href="https://netflixtechblog.com/single-tenant-titus-vm-level-isolation-on-a-multi-tenant-container-platform-6d8b1909d163">Single-tenant Titus: VM-level isolation on a multi-tenant container platform</a> was originally published in <a href="https://netflixtechblog.com">Netflix TechBlog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Ontology-Driven Observability: Building an End-to-End Knowledge Graph at Netflix Scale]]></title>
            <link>https://netflixtechblog.com/ontology-driven-observability-building-an-end-to-end-knowledge-graph-at-netflix-scale-421392cf5adf?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/421392cf5adf</guid>
            <category><![CDATA[knowledge-graph]]></category>
            <category><![CDATA[ontology]]></category>
            <category><![CDATA[genai]]></category>
            <category><![CDATA[observability]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Fri, 16 Oct 2026 16:01:02 GMT</pubDate>
            <atom:updated>2026-10-16T16:01:02.124Z</atom:updated>
            <content:encoded><![CDATA[<p><a href="https://www.linkedin.com/in/pvijayanathan/">Prasanna Vijayanathan</a>, <a href="https://www.linkedin.com/in/renzosanchezsilva/">Renzo Sanchez-Silva</a></p><p>When Jake Paul and Mike Tyson stepped into the ring in November 2024, we delivered the fight to roughly 65 million concurrent streams. On an ordinary day our infrastructure takes in more than 2 billion requests and handles upwards of 38 million real-time logging events every second. At that scale, the whole picture doesn’t fit into a single engineering team’s head or onto a single dashboard. With more Live content, Podcasts and real-time streams, our need to understand and update our systems as quickly as possible becomes imperative to provide a good user experience. Understanding the system at this scale and within the time constraints we have, often becomes a daunting exercise in finding a needle in a haystack, in a dark jungle, blindfolded.</p><p>This post is about that search for the needle.</p><h3>The Four hours before a fix</h3><p>Consider a regression of the kind we see in a given week. The home screen where you see the list of lists of movies, starts taking about 1.5 seconds longer to render. An alert fires. And then we go looking for what caused this issue and how do we fix it?</p><p>As it so happens, a regression on the application could be related to anywhere in the system — a client app regression from a new version deployment or A/B test rollout, a load shedding in the infrastructure, a GraphQL query responding with bad data because of a service rollout 6 hops downstream, are all suspects at this point. So, answering <em>“what caused the issue?”</em> needs insights from across the system, and this involves combing through layers of data in a meticulous manner.</p><h3>The MELT Layer</h3><p><em>“What caused the issue?”</em></p><p>Metrics, events, logs and traces, or MELT, in the industry’s shorthand, are the right raw materials for such an investigation, and each of them answers roughly a quarter of the question. We collect observability data across these four pillars, unified into a single observability data layer. This lets us observe our systems in real time, across what our users are experiencing, how our applications are doing and what our backend infrastructure and services are processing and responding to.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oFq_6JphuwR2l1zG6i8J5A.png" /><figcaption>MELT — Metrics, Events, Logs and Traces, and how they are captured across the system.</figcaption></figure><p>The investigation that follows may look like this in each layer:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YFPCXLL1qt2gFmNTj5wpOQ.png" /><figcaption>What each layer of MELT tells us and what it leaves out</figcaption></figure><h3>The timeline of an investigation</h3><p>So how long does it take to investigate an issue, figure out the root cause and fix it, so our users stop running into this issue again?</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8BEaMy8uy3bqqAjTF3pOjA.png" /><figcaption>A timeline of an investigation: ~4 hours</figcaption></figure><p>In our chosen example, four hours. Very little of it went into computing anything difficult. Most of it went into reconstructing relationships that our observability systems already knew but we had never written down anywhere a human or machine could simply read and discover.</p><h3>The disagreement underneath</h3><p>The instinct here could have been, around 2023, to reach for more data or better dashboards, and while both help at the margins, neither addresses what makes these investigations slow.</p><p>It’s 2026 now, so we might say, “hey, agents”, but without the right context delivered to the agents, AI systems hallucinate, and especially when teams are on-call and need a fix “now”, we cannot let inaccurate agentic analysis lead us down the wrong path. We need the right needle, not any needle.</p><p>Each of our systems is internally coherent and externally isolated. <a href="https://github.com/Netflix/atlas">Atlas</a>, our telemetry platform, knows applications and their time series. Radar, our alerting system, knows thresholds and firing history. incident.io knows severities and responders. PagerDuty knows rotations. Spinnaker knows what shipped and when. And our Slack history knows, in a form no query engine can reach, which engineer solved something very similar last quarter and what they found.</p><p>The difficulty is that they do not agree on what they are talking about. A thing that is an <em>application</em> in one system is a <em>service</em> in a second and a <em>cluster</em> in a third, with names differing by a suffix, a casing convention, or a rename only a couple of people remember (SMEs, ring a bell?). Joining across them requires a human carrying the mapping in their head, and that person becomes the bottleneck in such cross-domain investigations. The hard part of end-to-end observability is probably better understood as a data modelling problem.</p><p>What we lacked was an explicit, machine-readable agreement about the entities in our operational world.</p><h3>The Ontology</h3><p>This agreement has a name: an <strong>ontology</strong>. Ontology is a formal specification of types, the properties they carry, and the relationships they may have. Its unit is the <strong>triple</strong>, one fact expressed as subject, predicate, object. Consider the following example triples.</p><pre>api-gateway   rdf:type       ops:Application .<br>api-gateway   ops:ownedBy    &quot;Team Bedrock&quot; .<br>api-gateway   ops:deployedIn &quot;us-east-1&quot; .<br><br>INC-5377      rdf:type       ops:Incident .<br>INC-5377      ops:affects    api-gateway .<br>INC-5377      ops:severity   &quot;SEV 2&quot; .</pre><p>In the example here, we have already encoded six facts, and can answer in one query, a question that used to span three tools.</p><p><em>“Which incidents affected applications owned by Team Bedrock in us-east-1?”</em> is a single query in place of looking up incidents, Spinnaker and Radar.</p><p>We call our ontology layer, The <strong>Observability Ontology.</strong> It acts as the schema, rule set and shared agreement that standardizes how different systems understand entities like applications, services, incidents, deployments, etc.</p><p>We built it on the W3C stack for this: <a href="https://www.w3.org/RDF/"><strong><em>RDF</em></strong></a> for the data model, <a href="https://www.w3.org/2001/sw/wiki/OWL"><strong><em>OWL</em></strong></a> for the ontology, <a href="https://www.w3.org/TR/shacl/"><em>SHACL</em></a> for validation, and <a href="https://www.w3.org/TR/sparql11-query/"><em>SPARQL</em></a> for querying. These standards are well specified and widely implemented, which keeps the model portable across graph stores.</p><p>The data from the ontology itself, is stored as a knowledge graph, persisted to QuipuDB, our homegrown RDF graph database, a distributed quad store based on <a href="https://www.redb.org/">redb</a>, a Rust-based embedded key value db. Data here is generated by parsing raw telemetry, logs, and incident discussions into triples (<strong><em>subject-predicate-object</em></strong> facts).</p><p>Every harvest also produces a full Turtle serialization for archival, making it human-readable and diff-friendly, which has proven more useful for debugging than we expected.</p><p>Our ontology is organized into twelve namespaces, each owning one operational domain:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RQ_OG2v2NBva79ErwWtfdA.png" /></figure><p>There are two decisions about this ontology that mattered while we built it, so they’re worth sharing.</p><p>The first is that we refused a generic Service type. “Service” means four different things across our tooling, so applications resolve to exactly one type, ops:Application, and every one of them is validated against our software catalog of roughly 49,000 entries before the entity is created. Unvalidated strings like usernames, team names, dependency names scraped from network topology, never become applications. We also cap dependency edges in both directions, because one popular service can carry thousands of reverse dependencies and would otherwise dominate the graph.</p><p>The second decision concerns what we chose to model. Incident chat carries the <em>reasoning</em> behind an incident. The hypotheses tried and discarded, the mitigation that worked, who knew why, is all context in the incident’s Slack channel and that knowledge evaporates when the channel goes quiet. So a 200-message, 50-person, 15-service incident channel becomes a structure, converting the operational chaos of an incident into ordered triples:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-e5b7KxpaHth1viAJBn3sg.png" /></figure><h3>Creating determinism from non-determinism</h3><p>Building this graph means using tools that are inherently stochastic. LLMs summarize discussions. Fuzzy matching resolves service names. A probabilistic engine proposes causal chains. None of these produce the same output twice with certainty, but what comes out at the end of our system is a deterministic graph that answers a SPARQL query the same way every time.</p><p>The ontology is the mechanism that creates this determinism through its pipelined approach. We think of the pipeline as a ladder that drives entropy down, where entropy means the proportion of information still trapped in unstructured form. For example, at the start of this pipeline, an LLM could say anything, but it cannot emit a triple that violates the type system. We constrain the output space tightly enough and a stochastic process yields a deterministic contract:</p><pre># The model may produce any prose. Its output MUST fit these predicates:<br>category  IN [&quot;incident&quot;, &quot;question&quot;, &quot;discussion&quot;, &quot;decision&quot;]<br>sentiment IN [&quot;positive&quot;, &quot;neutral&quot;, &quot;negative&quot;, &quot;urgent&quot;]<br><br># Result — a deterministic triple:<br>thread:123  slack:category          &quot;incident&quot; .<br>thread:123  slack:isIncidentRelated &quot;true&quot;^^xsd:boolean .</pre><p>There are five layers that get us down this ladder. Any single step of this ladder is non-deterministic. But in aggregate, the chain becomes deterministic. That inversion is the whole trick. We describe them below:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6w2aBzfjw69aM92HpbBaAA.png" /><figcaption>The five layers to create determinism from non-determinism</figcaption></figure><h3>Enrichers: Agents as entropy reducers</h3><p>We use what we call, enrichers, to reduce entropy across the knowledge graph. Dozens of enrichers run over the graph. Each one of them takes noisy input and emits typed triples, and each reads what earlier enrichers wrote. The contract for an Enricher is deliberately narrow:</p><pre>class Enricher:<br>    async def _enrich_async(self, ...):<br>        raw    = await self.fetch(...)      # 1. read noisy input<br>        result = self.analyze(raw)          # 2. process (may be stochastic)<br>        graph.add((uri, RDF.type, OPS.X))   # 3. emit TYPED triples<br>        graph.add((uri, OPS.pred, Literal(result.value)))<br>        return EnrichmentResult(success=True, triples_added=count)</pre><p>Three invariants that are standard across the enrichers:</p><ul><li>every entity carries an <em>rdf:type</em>,</li><li>every URI follows a single naming scheme, and</li><li>the catalog gates every <em>ops:Application</em>.</li></ul><p>Two operational choices are worth naming here, as well. The enrichment pipeline runs sequentially — load the graph, add triples, and save.</p><p>This sequencing removes an entire category of race conditions at a cost in wall-clock time we are happy to pay. And calls out to external systems like metrics, logs, events and the rest could be best-effort, so a slow downstream API doesn’t block the graph harvest, albeit some degradation. To that end, we are also working to translate these separate systems into the ontology, such as the Metrics Ontology and the Alerts Ontology, so everything could be queried from the Ontology itself.</p><h3>The Correlation Engine: finding the correlation chains</h3><p>The correlation engine assimilates the vast number of signals from diverse input sources and condenses them into causal or correlatory chains of events. Today, the engine reads thirteen signal types, including log errors, metric anomalies, slow and failed traces, alerts, incidents, deployments, config changes, experiments, autoscaling, commits, end-to-end bottlenecks, and error patterns. It then searches for paths among them. Some ordered pairs are known to be causal and carry a high prior weight. For example, deployment before log error, config change before metric anomaly, failed trace before incident, and so on. Others, which show high correlation are considered correlation chains. Such chains are scored on temporal proximity and severity, inside a ten-minute window, with anything under 0.6 confidence getting discarded.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jVcQa_dQRUXtWwxNgoVepA.png" /><figcaption>The Correlation Engine</figcaption></figure><p>The role of the engine here is to propose with evidence, not to make conclusions. Hence, the engine stores its chains along with the graph records that were used to create them.</p><p>A <strong><em>0.87</em></strong> chain in the above example, is a strong hypothesis for a human or an agent to check, and the graph records the confidence alongside the claim so that consumers can understand why a chain is what it is and weigh their options to make decisions.</p><h3>The knowledge flywheel</h3><p>Each harvest cycle turns the same four steps, and each rotation leaves the graph richer for the next. This is the Knowledge Flywheel, leading to continuous enrichment and improvement of the context about our system, stored in the knowledge graph.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*W-yWjP_jlVKsGT5L81YjiA.png" /><figcaption>The Knowledge Flywheel</figcaption></figure><p>Observe collects the signals. Enrich adds typed triples. Infer finds patterns invisible in the raw data. Adapt feeds knowledge back, so that established patterns become detection rules. The compounding result of the four phases leads to a self-improving system. A causal chain discovered in one incident becomes a known pattern for the next, and the ontology gradually stops being a schema and starts being a memory.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*B38gUYJ6jRy8x5mEJYD89g.png" /></figure><p>Consider the example above. When we first see an incident in api-gateway caused due to a timeout and resolve it, we convert this knowledge into entries to the ontology. This becomes encoded knowledge. As we go through multiple cycles of this encoding, the graph starts learning patterns. After N cycles, patterns become wisdom — <em>“api-gateway deploys cause 40% of SEV-2s”</em> and <em>“30% of deployment issues in api-gateway come from Connection Timeouts”</em>. Surface this as an insight and we proactively start reducing incidents, not just react quickly to new ones.</p><h3>Putting the graph to work</h3><p>On top of the graph sits <em>AutoRCA</em>, our internal agentic AI solution, built with a set of specialist agents coordinated around one question. Walking the home page regression from the top of this post, the agentic system’s reasoning chain might look something like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xRVtYEGcuAqHbJmh99QXTA.png" /></figure><p>Each specialization to the inputs is built by people who understand their respective domain deeply. That division of labour only works if the specialists can pass findings to one another without having to agree on the terms of communication or even have knowledge of each other. The ontology acts as the interface. Each specialist learns one vocabulary — the graph’s. A team writing specialized use cases for how they use traces record their findings as triples about an ops:Application, the logs specialization reads triples about that same ops:Application, and neither needs to know the other exists. A new specialist learns the same vocabulary once and can immediately read everything the other six have written.</p><p>Set against the investigation we opened with, the change is narrower than it might sound. The alert still fires at 3:30pm, an engineer still has to weigh the evidence, and the fix still has to be written by someone who understands the code. The hour or two that used to go into assembling context — finding the owning team, reconstructing what shipped, correlating information from different parts of the system, recalling that something similar happened last quarter — all that work is the part the ontology and the graph absorbs. We are early enough that the ranked hypotheses are sometimes wrong, which is why each one carries the evidence that produced it. But when the engineer now starts the investigation, they are already holding the picture that used to take half an afternoon to assemble.</p><h3>If you want to build one of these</h3><p>A fair amount of what we have built, we learned by getting it wrong first. Based on this experience, these are the principles we would carry into a second attempt.</p><ul><li><strong>Model identity before anything else.</strong> Choose one authoritative catalog per entity kind and validate against it before creating an entity.</li><li><strong>Treat the ontology as code.</strong> Version it, review it, test it, and let changes arrive as pull requests. Treating the ontology as code lends itself to agentic development and lets teams scale and contribute to it.</li><li><strong>Separate concepts from instances.</strong> Concepts change on a human timescale and instances change continuously, conflating them makes both harder to evolve.</li><li><strong>Build on open standards.</strong> RDF, OWL, SHACL and SPARQL are specified, implemented many times over, and portable. A bespoke graph model ties a future consumer to one implementation.</li><li><strong>Constrain stochastic tools with a type system.</strong> This is the principle we would defend hardest. Models are useful precisely where determinism is impossible, and a sufficiently narrow output contract converts their output into something a query engine can trust.</li><li><strong>Make enrichment best-effort and ordered.</strong> A slow dependency should utmost degrade the graph, never block it. And ordering enrichers so each builds on the last buys more accuracy and that’s worth the tradeoff with parallelism that buys speed.</li><li><strong>Model human reasoning alongside machine telemetry.</strong> The most valuable edges in our graph came from discussion. Telemetry records what the system did, discussion records what people understood and interpreted. The reasoning is more valuable to store and serve as wisdom when we need it.</li><li><strong>Keep humans as the approvers.</strong> Let agents propose fixes, enrichers, and ontology changes, and route every proposal through a review process with humans in the loop.</li></ul><h3>What comes next</h3><p>The graph makes three things possible. First, automatic root cause analysis, where a regression arrives with a ranked set of graph-backed hypotheses attached. Next, automated remediation, where a sufficiently confident diagnosis carries a proposed action like a targeted rollback, a traffic shift, a code change. And eventually infrastructure that heals itself, acting on a predicted failure before members feel it.</p><p>Alongside this, we plan to integrate the Observability Ontology with <strong><em>Upper</em></strong>, the domain-modeling language at the heart of <a href="http://medium.com/r?url=https%3A%2F%2Fnetflixtechblog.com%2Fuda-unified-data-architecture-6a6aee261d8d">Netflix’s Unified Data Architecture (UDA)</a>. Upper lets teams describe business and system domains as interconnected, machine-readable models and preserve their meaning and constraints as those models are projected into concrete schemas and systems. This integration would connect operational concepts such as applications, alerts, deployments, and incidents to the broader set of domains modeled across Netflix, giving engineers and agents a shared semantic layer for querying, validation, and reasoning. We will begin with a proof of concept that models incidents in Upper and use what we learn to determine where the two systems should converge.</p><h3>Acknowledgements</h3><p>None of this would exist without the teams who share their data, expertise, and patience with an early system. We want to specially acknowledge the efforts of our own team members who made this possible from very early on in this lifecycle, <a href="https://www.linkedin.com/in/kaela-marsheck-788948243/">Kaela Marsheck</a> and <a href="https://www.linkedin.com/in/adityagaur/">Aditya Gaur</a>. We are also especially grateful to our collaborators in Netflix’s ontology working group, Peter Royal, Alex Hutter, and to Alexandre Bertails and members of the Unified Data Architecture (UDA)/Knowledge Foundations team for sharing Upper and their domain-modeling expertise, and for helping us explore how the Observability Ontology can become part of a broader semantic foundation at Netflix.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=421392cf5adf" width="1" height="1" alt=""><hr><p><a href="https://netflixtechblog.com/ontology-driven-observability-building-an-end-to-end-knowledge-graph-at-netflix-scale-421392cf5adf">Ontology-Driven Observability: Building an End-to-End Knowledge Graph at Netflix Scale</a> was originally published in <a href="https://netflixtechblog.com">Netflix TechBlog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Reliable GPU compute for large training & inference jobs]]></title>
            <link>https://netflixtechblog.com/reliable-gpu-compute-for-large-training-inference-jobs-d62b03a96d7a?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/d62b03a96d7a</guid>
            <category><![CDATA[site-reliability]]></category>
            <category><![CDATA[gpu]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[cost-efficiency]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Wed, 14 Oct 2026 16:01:04 GMT</pubDate>
            <atom:updated>2026-10-28T06:10:23.584Z</atom:updated>
            <content:encoded><![CDATA[<p><em>How we detect failing GPUs and network fabric, remediate machines automatically, and let long-running jobs survive hardware loss.</em></p><p>Authors: <a href="https://www.linkedin.com/in/nick-rutigliano/">Nick Rutigliano</a>, <a href="https://www.linkedin.com/in/argha-c/">Argha C</a>, <a href="https://www.linkedin.com/in/samartha/">Samartha Chandrashekar</a></p><p>Open Netflix, and the home screen is assembled for you: which rows appear, the order they appear in, the titles within them, and the artwork for each title are chosen by machine learning models trained on how hundreds of millions of members watch. Personalization is one of the most widely used things we build, and members feel it every time they open the app. Like a growing share of Netflix, it runs on GPUs, which train those models and increasingly serve them live.</p><p>Those GPUs have to stay healthy for that experience to hold up, and keeping them healthy is harder than it sounds. A single large training job can run for weeks across hundreds of GPUs spread over many machines. It runs as a gang: the ranks move in lockstep, exchanging data every step, so the job advances only as fast as its slowest participant and stops entirely if one participant dies. Every GPU, every high-speed network adapter, and every interconnect must keep working throughout the run. At the scale at which modern machine learning runs, individual GPUs and network links can fail during a run, so the underlying platform must be prepared for this and handle it without a person in the loop.</p><p>This post describes how we keep our GPU infrastructure healthy for training and inference. We cover how we detect failing hardware before and during jobs, how we automatically remediate a bad machine, and how we let a long job survive the loss of a machine. These training workloads run on our modern Kubernetes-native Compute platform.</p><h3>Why GPU reliability matters</h3><p>Netflix aims to entertain the world, and machine learning has been part of how we do that for years, from the recommendations that help members find something to watch to the systems that help produce and promote titles. As generative AI advances, we are investing in it on both sides of the business: for members, through better recommendations and higher-quality promotional assets; and for storytelling, through creative tools for our production partners.</p><p>All of this runs on GPUs, for both training and inference, and GPU capacity is among the fastest-growing and most expensive parts of our infrastructure. That makes reliability a business concern on two fronts.</p><ul><li>Availability: a training run that loses a machine, or an inference service that cannot get healthy capacity, delays work that members and creators depend on.</li><li>Cost efficiency: a GPU sitting idle behind a slow neighbor, or a whole machine pulled from the pool because one device is degraded, becomes capacity we paid for and are not using. Higher reliability and higher utilization mean more useful work per GPU, which is equivalent to spending less to do the same work.</li></ul><p>Inference is a growing share of that GPU footprint as more of these AI features reach members, so the reliability and efficiency that matter for multi-week training runs increasingly matter for always-on serving too.</p><h3>Why basic health checks are not enough</h3><p>We start from the baseline provided by the cloud provider. Cloud providers run instance status checks on virtual machines: a system status check for the underlying host’s power, network, and hardware reachability; an instance status check to verify that the operating system is up and reachable; and a check on attached storage. It emits scheduled maintenance events when it plans to retire or reboot a host, and it can automatically recover an instance that becomes impaired. We use all of these, and the maintenance events feed one of our own checks. They catch a machine that is down, unreachable, or slated for maintenance, and they stop at the instance level. They confirm the box is up and reachable. They do not verify that each GPU, its on-chip memory, its interconnect, and its network fabric are healthy enough to support distributed training.</p><p>The next step most teams take is a GPU-level check to confirm that the driver responds and that the GPUs are visible. That catches a dead or missing GPU, but misses the cases that take the most time.</p><ul><li>A GPU can answer a status query and still be faulty once a workload touches it. A fault in on-chip memory or in the firmware that manages the device can remain invisible until a job allocates memory or runs sustained compute, at which point the job fails or slows.</li><li>A high-speed network adapter may be present but show fewer links than expected. Collective operations still run, at a fraction of the bandwidth, so the job does not crash. It crawls, and the cause is easy to miss.</li><li>A check can fail in ways that leave a faulty machine looking healthy. The diagnostic tool can hang when the driver hangs, and a naive check that waits on it hangs too. A binary that the check depends on can go missing after a driver upgrade, causing the check to stop running and report nothing. A check that can silently stop running is worse than no check, because it gives false confidence while the machine keeps working.</li></ul><p>Because a job runs as a gang, any one of these on any one machine can degrade or stop the whole job, and none of them shows up in an instance-level check or a driver-responds check. The health system has to assume the hardware can lie and the tools that inspect it can break.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d15z8m06XjWnYeauK7iXfQ.png" /><figcaption><em>A machine can pass a basic check and still be faulty. Layered checks, timeouts, a fallback path, and checks that fail when a dependency is missing close these gaps.</em></figcaption></figure><h3>Our approach</h3><p>We treat reliability as four layers, each catching what the layer below cannot:</p><p><strong>1/ Node and hardware health:</strong> find bad GPUs, network adapters, interconnects, memory errors, and thermal issues, then taint or recycle the machine.</p><p><strong>2/ Scheduler and platform:</strong> retry at the task &amp; job level, validate a machine before it takes work, and drain &amp; recycle machines safely.</p><p><strong>3/ Training loop: </strong>checkpoint and use elastic workers so a job survives losing a machine.</p><p><strong>4/ Observability:</strong> metrics, service-level objectives, and failure attribution across the other three.</p><p>The first two layers keep hardware problems away from jobs. The third accepts that some loss is unavoidable and makes it cheap. The fourth tells us whether the first three are working.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2lLAu7fFc5rqqiWY2QAbhA.png" /><figcaption><em>Four layers: Each catches what the layer below cannot.</em></figcaption></figure><h3>How the health checks work</h3><p>Checks run in two tiers. Device-level checks look at each GPU. A node-level agent, built on the open-source Kubernetes <a href="https://github.com/kubernetes/node-problem-detector">node-problem-detector</a>, runs on every GPU machine every 30 seconds and turns check results into conditions on the Kubernetes node, the same mechanism Kubernetes uses to mark a node NotReady. Controllers watch those conditions and decide whether the machine can take on work.</p><p>GPU device checks are built on NVIDIA’s <a href="https://developer.nvidia.com/dcgm">DCGM</a> for driver-reported error codes, uncorrectable memory errors, interconnect integrity, and thermal state, and networking adapter checks inspect the host state for the count and link state of the <a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/efa.html">Elastic Fabric Adapter</a> interfaces that carry cross-machine traffic. A few of these checks deserve detail.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*73vlwnPR6M7b1cYx43QeGw.png" /><figcaption><em>Two-tier health checking. Device-level and node-level checks become Kubernetes node conditions, which controllers then turn into taints, drains, terminates, and replaces.</em></figcaption></figure><ul><li><em>Failing closed:</em> if the driver or the diagnostic tool is unresponsive, the check times out and marks the machine unhealthy. A check that cannot reach the hardware reports a problem rather than passing, so a wedged driver takes the machine out of rotation rather than letting it hide behind a hung tool.</li><li><em>A fallback path:</em> driver error codes are the strongest signal that a GPU has faulted, but the tool that reads them can itself be broken in certain failure modes! When it does, we read the kernel log dmesg directly for those codes, so no Xid or SXid errors are missed.</li><li><em>Our own fatal-error map:</em> vendor driver error catalogs lag newer GPU generations and do not always match what we see in production, so we keep our own mapping of which codes are fatal for each GPU family. A code that means reset-the-GPU in one generation can be benign in another, and one GPU generation emits a burst of normally-fatal code on every healthy boot, which we learned to treat as noise rather than drain fresh machines.</li><li><em>No silent no-ops:</em> a check fails when a required dependency is missing. This one came from an upgrade that dropped a binary the network-adapter check relied on. The check quietly stopped running, and machines with the wrong number of adapters shipped for days before the slow jobs were traced back to them. Now a missing dependency fails the check.</li></ul><h3>Turning a bad machine into healthy capacity, automatically</h3><p>When a check fails, the machine’s node condition changes, and controllers take over. We distinguish two levels. A problem condition blocks new work from landing while jobs are running, because pulling a job off a machine is disruptive, and many problems clear on their own. If the condition persists beyond a tolerance window, it escalates to a failure condition, which evacuates the machine, drains its tasks (overriding the normal disruption limits to move quickly), terminates it, and replaces it.</p><p><a href="https://karpenter.sh/">Karpenter</a> node auto-repair is used on our compute stack. We have customized it with a policy and a controller that maps each node condition to the duration we tolerate it before acting, so a fast-fatal error is actioned within minutes. At the same time, a flaky signal gets a longer window, and a cap limits how much of a cluster we will disrupt at once, so a bad rollout of the checks themselves cannot cascade into tearing down a whole cluster. We plan to upstream this feature to all Karpenter users.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*E194fQZ3PRUDlq0AU5VQPg.png" /><figcaption><em>Graded remediation. A problem is tolerated and may clear; a persistent failure escalates to evacuation and draining, then to a fast in-place fix or termination for confirmed hardware faults.</em></figcaption></figure><p>Not every fault requires a full replacement, and replacement is the slowest way to restore capacity. We match the action to the fault. Killing the offending job clears a software wedge. Resetting a single GPU clears many transient device faults. Rebooting the machine clears the firmware state that has drifted over a long uptime. Terminating and replacing is reserved for hardware that fails a test we trust. For that last case, we record the GPU’s immutable hardware identifier, which stays with the physical device across reboots and relaunches even when the instance changes, report it to the cloud provider, and keep our own list, so a device that fails repeatedly does not quietly rejoin the pool under a new instance.</p><p>One subtlety sits between detection and scheduling. A freshly checked machine was handed to the scheduler just as its checks were still settling, so a job could land on it a moment before a slow check flagged it. We now hold capacity until the checks have run and settled, which closed that window.</p><h3>Validating a machine before it takes work</h3><p>A machine that just booted is not trusted until it proves itself. Before we release its capacity, it passes system checks, confirms the GPUs and their topology are present, and runs a round of GPU diagnostics. We then wait for the health checks to run twice, because the longer checks, such as counting network adapters, take more than one cycle to fire, and releasing after a single pass would let work land before those checks are reported. Only then do we remove the startup taint. On teardown, we run deeper diagnostics and record the GPU identifiers, which feed the same bad-hardware list, so hardware that failed once does not silently cycle back into service.</p><h3>Letting long jobs survive machine loss</h3><p>Even with all of the above, some machines are lost mid-job, so we make that cheap. Jobs checkpoint their state, and on a lost machine, they resume from the last healthy checkpoint rather than restarting from the beginning of a multi-week run. Workers are elastic and retryable, and the scheduler automatically retries platform-caused failures.</p><p>Attribution is what makes automatic retries safe. If we retried every failure, we would mask genuine bugs in a job and burn GPU hours looping on an error the platform cannot fix, so we classify each failure as platform or application before retrying. One example of why this matters: a class of timeouts in <a href="https://developer.nvidia.com/nccl">NCCL</a>, the collective communication library GPUs use to exchange data, appeared to be a network fault and was traced to data-loading stalls at high worker counts. The GPUs and the network were fine, and the fix was in the data pipeline. Treated as hardware failures, those timeouts would have cycled healthy machines without fixing the cause.</p><h3>Measuring whether it works</h3><p>We track GPU reliability along three service-level objectives, each split by cause, so a miss points at the layer responsible:</p><ul><li><em>Availability:</em> the amount of GPU capacity available for scheduling at any given moment.</li><li><em>Job-start success:</em> whether a job starts promptly once its capacity is available.</li><li><em>Job success:</em> whether a job finishes without a platform-caused failure.</li></ul><p>Each objective is split into hardware, driver, platform, and user causes. Hence, an availability dip from bad hardware reads differently from one caused by a scheduler change or a user misconfiguration. We optimize for goodput, the useful work the fleet completes, because a job held back by one slow GPU can look healthy on an uptime chart while getting almost nothing done. Specific targets for each objective are provided here for illustrative purposes.</p><h3>A closed remediation loop</h3><p>These pieces form a loop. Hardware is validated at allocation, before a job starts, with a short collective communication run and pairwise network tests. Hence, any machine that fails within the first minute fails before the job is admitted. During the job, we watch passively for driver errors, memory errors, thermal events, and more. We view each job as a distributed system, which lets us identify a single slow rank dragging the rest, rather than only whole-machine failures. When something is wrong, remediation is automatic and graded. The loop is the same whether a job runs through our batch scheduler, <a href="https://kueue.sigs.k8s.io/">Kueue</a>, or <a href="https://www.ray.io/">Ray</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UpFBA2LWwDLkaujStvIq_g.png" /><figcaption><em>The reliability loop: validate, watch, actively check, view the job as a system, remediate, then repeat.</em></figcaption></figure><h3>Sharing GPUs safely</h3><p>Not every workload needs a whole GPU, and leaving small inference jobs on dedicated GPUs wastes capacity. We partition GPUs with <a href="https://docs.nvidia.com/datacenter/tesla/mig-user-guide/">MIG</a> for memory-level isolation between tenants, so a fault or a memory hog in one partition does not affect another. We also reconfigure those partitions without restarting the machine, so that a pool can shift between many small slices and a few large ones as demand changes. GPU and network adapter allocation is device-plugin-based today, to move to Kubernetes Dynamic Resource Allocation once it is supported in Karpenter. This matters because a GPU talking to a network adapter or working with a CPU on the wrong PCIE root of the machine incurs a bandwidth penalty.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1MA62iL5MCRimp5V1Fgmrw.png" /><figcaption><em>On a multi-GPU machine, we isolate a single faulty GPU, keep the rest in service, and partition healthy GPUs across multiple tenants with memory-level isolation.</em></figcaption></figure><p>Device-level allocation also changes how we handle a single bad GPU. With DRA support, instead of losing the whole machine, we could isolate the one faulty device and keep the others serving, which matters most for inference, where many small jobs share a machine. And because allocation is expressive, a job can request the capabilities it needs, such as memory or tensor-core support, and the platform matches a suitable GPU type, so users are not pinned to a model they had to choose in advance.</p><h3>Results</h3><p>Moving from basic checks and manual draining to layered checks and automatic remediation changed two things users feel. More of the GPU fleet is available for scheduling at any time because bad machines are caught and cycled quickly, rather than sitting in the pool or waiting on an on-call engineer. And fewer jobs fail due to platform issues because most faults are caught before a job lands, and the rest are automatically remediated and retried. Graded remediation also returns capacity in minutes for faults that only require a job kill, a GPU reset, or a reboot, rather than the longer wait for a replacement machine.</p><p>Since the above improvements have landed, we’ve seen:</p><ul><li>Consistently achieving &gt;99.95% availability of our GPU nodes</li><li>Job success rate has gone from ~98% to above 99.5%</li></ul><h3>What is next</h3><p>We are extending active checks to cover additional failure modes under load, widening burn-in so more classes of failures can be cleared before a user encounters them, and making remediation more granular so more error types are cleared in place rather than requiring a full node replacement loop for certain Xid errors. Additionally, we want to do more periodic diagnostics between jobs.</p><p>We are working to make placement more topology-aware using Kubernetes Dynamic Resource Allocation (DRA) once it lands in Karpenter, for example, by aligning on Compute/IMEX Domains, PCIe roots, and NUMA nodes.</p><p>We also plan to run a custom Linux scheduler built on <a href="https://docs.kernel.org/scheduler/sched-ext.html">sched_ext</a>. This enables us to prioritize processes during collective coordination, such as when GPUs exchange results before the next round of compute, thereby reducing overall GPU idle time by establishing a fleet-wide priority for these tasks. The effect is small on one machine but grows across hundreds of machines, where a few percent of throughput amounts to a large amount of capacity. Additionally, it allows for prioritizing data-loading processes to minimize bottlenecks that occur when data is not loaded onto GPUs quickly enough to maintain steady progress.</p><p>While we already monitor throttling events, GPUs do not have to report throttling for it to hurt a job. One running ten to fifteen degrees hotter than its peers, even within healthy limits, sets the pace for the gang, so we can treat a large temperature gap as a fault, stop the job, and move it to cooler GPUs.</p><p>Additionally, we plan to expand our NCCL RAS (reliability, availability, serviceability) monitoring to identify NCCL issues sooner, such as mismatched collectives or hangs, to more quickly resume work.</p><h3>Closing</h3><p>Hardware fails, and at this scale, it fails often enough that the platform has to treat failure as normal. Detecting problems before and during jobs, automatically remediating machines, and allowing long jobs to resume after a loss lets teams run large-scale training and inference without babysitting the hardware. If you like these kinds of problems, we are hiring.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d62b03a96d7a" width="1" height="1" alt=""><hr><p><a href="https://netflixtechblog.com/reliable-gpu-compute-for-large-training-inference-jobs-d62b03a96d7a">Reliable GPU compute for large training &amp; inference jobs</a> was originally published in <a href="https://netflixtechblog.com">Netflix TechBlog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Trading a Cloud Identity for Your Own: Workload Attestation on Managed Compute]]></title>
            <link>https://netflixtechblog.com/trading-a-cloud-identity-for-your-own-workload-attestation-on-managed-compute-516d5a29b252?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/516d5a29b252</guid>
            <category><![CDATA[identity]]></category>
            <category><![CDATA[aws-emr]]></category>
            <category><![CDATA[apache-spark]]></category>
            <category><![CDATA[cloud]]></category>
            <category><![CDATA[security]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Fri, 25 Sep 2026 16:01:02 GMT</pubDate>
            <atom:updated>2026-09-25T16:01:02.900Z</atom:updated>
            <content:encoded><![CDATA[<p><em>By </em><a href="https://www.linkedin.com/in/dhruvpratap/"><em>Dhruv Pratap</em></a></p><h3>Introduction</h3><p>Organizations that have been around for a while usually run two identity systems side by side. One belongs to the cloud provider: IAM roles, instance profiles, execution roles. The other is your own, and it is the one your internal services actually check when they decide whether to answer a request.</p><p>On infrastructure you build yourself, you can bootstrap your own identity however you like. On managed compute you cannot. The provider hands your process a cloud identity and nothing else.</p><p>This post describes how we close that gap for Apache Spark workloads running on Amazon EMR. A workload that starts with only an AWS identity has to end up holding a first-class internal identity. Doing the exchange is straightforward. Making it trustworthy is the part that took the design work. Very little of what follows is specific to Spark or to EMR.</p><h3>Two identity systems</h3><p>Internal service-to-service authentication at Netflix runs on a private PKI called Metatron. Every workload gets a short-lived X.509 certificate, and services authenticate each other with mutual TLS. A workload cannot simply ask for a certificate. It is issued only after the identity service has satisfied itself that the requester really is the workload it claims to be. That verification step is called attestation. In each environment we support (VMs, containers, functions) attestation rests on some environment-specific proof that the platform can check on its own.</p><p>The second concept is the Data Project, which is our unit of ownership for data. A Data Project owns tables, has a set of authorized users, and has an identity of its own. Jobs run as the Data Project rather than as the person who launched them. That is what keeps table-level access control and audit consistent across scheduled and interactive use.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eIWpHw7J_6qZCuaffD0lQQ.png" /></figure><p>So the problem is this. A Spark job on managed compute starts with an AWS execution role and no internal identity. Everything it needs from inside the company (reading an encrypted column, evaluating table ACLs, holding an interactive session open longer than a short-lived token lasts) requires the internal one.</p><h3>One identity, one role</h3><p>The design rests on a decision made outside the Spark stack entirely. Each Data Project identity maps 1:1 to a dedicated IAM role, and the service that owns Data Project metadata records that mapping.</p><p>The mapping is what makes translation possible. It lets a statement in the provider’s vocabulary (“<em>this process is running as role R</em>”) become a statement in ours (“<em>this process is workload W</em>”). Without it there is nothing to translate into, and no amount of cryptography helps. The hard part of bridging two identity systems is rarely the protocol. It is committing to a mapping and then keeping it authoritative.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tgftcx71a3qYoha0htBrDg.png" /></figure><p>One practical objection arrives immediately. A single AWS account cannot hold tens of thousands of IAM roles, and we expect data projects in the order of ten thousands. We handled this by sharding data project roles deterministically across a small pool of dedicated accounts, which scales well past the projected number of projects and has the useful side effect of keeping workload roles on the far side of an account boundary from the control planes that launch them.</p><h3>Components</h3><p>Five components take part.</p><p>The <strong>control plane</strong> is the only service allowed to launch Spark jobs. It resolves the Data Project’s IAM role, builds and signs a workload metadata payload, and submits the job with that role as the execution role. It holds a signing key issued to it for this purpose alone.</p><p>The <strong>Data Project service</strong> is the authority for the identity-to-role mapping.</p><p>The <strong>Identity service</strong> receives attestation requests, verifies them, and issues certificates.</p><p>A <strong>Spark plugin</strong> provides the hook. Spark 3.0+ added a plugin interface with driver-side and executor-side components that are initialized while those processes are bootstrapping.</p><p><strong>AWS STS</strong> acts as a notary, which is not its usual job.</p><h3>Step one: the control plane makes a signed claim</h3><p>When a job is submitted, the control plane validates the caller, looks up the Data Project’s IAM role, and builds a small payload describing the workload it is about to launch. The payload names the application identity, the mapped role, and the environment, stack and detail that the resulting credentials should be scoped to. The control plane signs the payload and passes it and the signature through as ordinary job configuration.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yKCL4VXtyj7lIYq48yk3fA.png" /></figure><p>Two properties of this step matter. First, only one service can produce a valid signature, so the set of things that can assert “<em>this workload is legitimate</em>” is small and auditable. Second, the payload is a claim rather than an authority. On its own it proves nothing, because job configuration travels through infrastructure we do not fully control and anything that can read it can replay it.</p><h3>Step two: the workload proves it holds the cloud identity</h3><p>When the managed service launches the driver process, it does so under an OS user that has the execution role’s credentials available. The driver-side plugin initializes and uses those credentials to sign, but not send, a request to the provider’s identity endpoint (sts:GetCallerIdentity). The result is a short-lived pre-signed URL.</p><p>That URL is a transferable proof of possession. Anyone can fetch it. Only the holder of the role’s credentials could have produced it. And the response comes from AWS rather than from the workload, stating which role signed the request. The workload cannot lie about the answer because the workload does not supply the answer.</p><p>The pattern is not new. It is the same idea behind AWS IAM authentication in HashiCorp Vault and in AWS’s own function attestation flows. It generalizes well: any environment that gives a process cloud credentials and nothing else can still produce a verifiable statement about its own identity.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*m5U08Ss3STVhPPvyOVNYMQ.png" /></figure><p>The plugin sends one attestation request carrying both artifacts, the pre-signed URL and the signed metadata.</p><h3>Step three: corroboration</h3><p>The identity service does four things.</p><ol><li>It fetches the pre-signed URL against AWS STS and reads back the role that signed it. This is the provider’s statement.</li><li>It verifies the metadata signature against the key issued to the control plane. This is the platform’s statement.</li><li>It corroborates the two. The role AWS reports must be the role the control plane said it dispatched, for the identity the metadata names.</li><li>It issues certificates for that identity, scoped to the environment, stack and detail from the signed metadata.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KTwmy83KQ_HATNec2-AqFw.png" /></figure><p>Step 3 is the point of the whole design. Neither statement is sufficient by itself, and they fail in complementary ways. The provider’s statement is unforgeable but under-specified: it gives you a role, and a role is not a workload. It says nothing about why this process exists or what it should be allowed to be. The platform’s statement is well-specified but unverifiable on its own, for the reasons above. Put together, the signature says the workload was legitimately dispatched and the pre-signed URL says this really is it. Neither party has to trust the workload’s own account of itself.</p><p>There is a tension here that we settled deliberately, and other teams should expect to meet it. The provider’s answer identifies a role, not an application. Deriving an application name from a role name is fragile string matching, and the session identifiers the platform assigns are not under our control. We chose to take the application identity from the signed metadata and use the provider’s answer only for corroboration. That costs a network round trip and a stricter signing requirement, and it buys a much clearer trust story.</p><h3>The fan-out problem</h3><p>A Spark application is one driver and up to thousands of executors, created and destroyed throughout the life of a job. Attestation designed around one process per host does not survive that.</p><p>There are two defensible answers.</p><p>In the first, each executor attests independently. This is uniform, adds no new trust boundary, and grounds every process’s identity in the same provider-verified proof. The cost is amplification. One large job can produce thousands of STS calls and thousands of attestation requests in a burst, which looks a lot like an attack and creates a hard dependency on provider-side rate limits.</p><p>In the second, executors inherit credentials from the driver. The driver attests once and distributes credentials to executors over Spark’s internal RPC, which we configure with authentication and AES-GCM encryption so that only processes in the same application can take part. Load on the identity service stays constant. The cost is a second trust boundary and a driver that is now a credential distribution point.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Bof8PubPgvJWCG_ZXGW7og.png" /></figure><p>Considering our scale at Netflix here we went with the second approach. The general lesson is that identity amplification is a capacity question, and it is better to answer it on purpose than to have it answered for you during an incident.</p><h3>Lifecycle</h3><p>Certificates are short-lived by design, so bootstrapping is only half the work. On self-managed hosts a system service handles renewal. Inside managed compute there is no equivalent hook, so the driver JVM runs a timer that re-attests on a fixed interval, well ahead of expiry. Credential material is written to a temporary directory readable only by the process owner and removed when the process exits.</p><p>Executors do not refresh. They are usually short-lived, and an executor that somehow outlives its credentials can exit and be replaced, which is cheaper than making every executor a renewal client.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tgaBnH_XGzBq1TdEDC00cg.png" /></figure><p>The rule worth carrying into any similar system is that attestation has to be a repeatable operation rather than a bootstrap step. Anything that can only happen once at process start will eventually be the reason a long-running job dies nine hours in.</p><h3>Why this generalizes</h3><p>If you run your own identity system and are moving workloads onto managed compute, the specific services here matter less than the shape of the solution.</p><ol><li><strong>Anchor on one exchangeable primitive.</strong> A single authoritative 1:1 mapping between your identity and a provider identity is what makes translation possible. The rest is plumbing.</li><li><strong>Require two independent claims and corroborate them.</strong> One from the provider, unforgeable and under-specified. One from your control plane, well-specified and replayable. Trust the intersection and never either alone.</li><li><strong>Keep the signer scarce.</strong> If exactly one service can make the platform’s claim, your trust story fits in a sentence.</li><li><strong>Hook the layer you still own.</strong> On managed compute you rarely control the host or its init system, but you almost always control the runtime: a plugin, an agent, an entrypoint. Attestation belongs there.</li><li><strong>Decide the amplification policy on purpose.</strong> Distributed engines multiply every per-process operation by their parallelism.</li><li><strong>Make attestation repeatable and credentials short-lived.</strong> Renewal is a requirement, not a follow-up.</li></ol><p>What makes the result trustworthy is that no single participant issues an identity by itself. Not the workload, not the control plane, not the provider. And the workload, which is the party in the weakest position to be trusted, is never asked to vouch for itself.</p><h3>Acknowledgements</h3><p>Thanks to <a href="https://www.linkedin.com/in/amer-hesson-0886a5a5/"><em>Amer Hesson</em></a> for the Data Project abstraction, <a href="https://www.linkedin.com/in/nsiow/"><em>Nick Siow</em></a> for the Data Project IAM role sharding, and <a href="https://www.linkedin.com/in/doug-clark-877b0a391/"><em>Doug Clark</em></a> for Metatron attestation.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=516d5a29b252" width="1" height="1" alt=""><hr><p><a href="https://netflixtechblog.com/trading-a-cloud-identity-for-your-own-workload-attestation-on-managed-compute-516d5a29b252">Trading a Cloud Identity for Your Own: Workload Attestation on Managed Compute</a> was originally published in <a href="https://netflixtechblog.com">Netflix TechBlog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Netflix Stratum Media Processing: Automated Container Right-Sizing]]></title>
            <link>https://netflixtechblog.com/netflix-stratum-media-processing-automated-container-right-sizing-ddf9aa68a801?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/ddf9aa68a801</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[cost-optimization]]></category>
            <category><![CDATA[resource-management]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Fri, 25 Sep 2026 16:01:02 GMT</pubDate>
            <atom:updated>2026-10-19T09:24:41.018Z</atom:updated>
            <content:encoded><![CDATA[<p><em>By </em><a href="https://www.linkedin.com/in/vpidvolotska"><em>Violetta Pidvolotska</em></a><em> and </em><a href="https://www.linkedin.com/in/nmareddy/"><em>Naveen Mareddy</em></a></p><p><a href="https://netflixtechblog.com/the-netflix-cosmos-platform-35c14d9351ad">Stratum</a> is Netflix’s internal serverless media processing platform, shaped by over a decade of experience developing large-scale distributed systems. It powers a wide range of media workloads — including encoding, packaging, inspection, and quality scoring — behind playback, advertising, studio workflows, AI embeddings, and more.</p><p>Engineers develop Stratum functions by packaging application code and OS dependencies into OCI images. Stratum then schedules these containers as batch jobs on Netflix’s <a href="https://netflixtechblog.com/titus-the-netflix-container-management-platform-is-now-open-source-f868c9fb5436">Titus</a> compute platform, with upstream services triggering function executions.</p><p>Every day, Stratum manages hundreds of thousands of containers across bursty, high-throughput, and latency-sensitive workloads. As Netflix’s core media processing platform, this load needs to scale as new content formats enter the catalog, such as <a href="https://www.netflix.com/tudum/podcasts">podcasts</a>, <a href="https://about.netflix.com/en/news/what-we-watched-the-first-half-of-2026">live events</a>, <a href="https://about.netflix.com/en/news/2026-world-baseball-classic-most-watched-netflix-japan">sports</a>, <a href="https://about.netflix.com/en/news/netflix-upfront-2026-get-closer">ads</a>, or regional ingests like <a href="https://about.netflix.com/en/news/f1-and-tf1-live-channel-now-available-on-netflix-in-france">TF1 in France</a>.</p><p>To request resources for these functions, owners specify their CPU, memory, disk, and network limits as YAML configuration, as shown below:</p><pre>functionName: VideoEncoder.encodeV1<br>resources:<br>  numCpus: 8<br>  memoryInMB: 18000<br>  diskSizeInMB: 50000<br>  networkInMbps: 1000</pre><p>Maintaining optimal values manually is challenging amidst evolving Netflix infrastructure, EC2 instance upgrades, and application-level changes. Consequently, configs are often over-provisioned upfront or slowly drift away from their ideal values over time. Mis-sized configurations are a universal pain of large-scale serverless platforms, causing additional cognitive load for function builders and operational costs for platform owners.</p><p>In this post, we share how we built the Stratum Resource Tuner (SRT) — an automated system that safely right-sizes running containers in production without human intervention. We will walk through real-world examples, our safety architecture, and what we learned when an automated change went wrong.</p><h3><strong>The Challenge: Over-Provisioning</strong></h3><p>When engineers write a new Stratum function, they typically copy a resource YAML from an existing function or size it for the absolute worst-case scenario. Once running in production, engineers rarely revisit these configurations until a failure occurs, prompting them to over-provision resources out of an abundance of caution.</p><p>Manual resource tuning does not scale for the following reasons:</p><ul><li><strong>High Operational Friction:</strong> Downsizing requires days of manual metric monitoring and trial-and-error testing, diverting valuable engineering cycles away from other high-impact work. If a cut goes too far, engineers face production incidents — making manual tuning a high-risk, low-reward task.</li><li><strong>Continuous Infrastructure &amp; Codec Evolution:</strong> Cloud providers regularly upgrade their hardware. A resource allocation that was optimal on an older hardware generation quietly becomes inefficient on newer hardware. At the same time, Netflix continuously advances media standards and improves encoding algorithms (such as <a href="https://netflixtechblog.com/av1-now-powering-30-of-netflix-streaming-02f592242d80">AV1</a>, AV2, and <a href="https://about.netflix.com/news/netflix-spatial-audio">spatial audio</a>). An allocation tuned for one codec might not be optimal when an improved algorithm is released.</li><li><strong>Dynamic Workload Evolution:</strong> As Netflix expands into new media types — such as streaming <a href="https://www.netflix.com/tudum/podcasts">video podcasts</a>, sports, interactive <a href="https://about.netflix.com/news/netflix-upfront-2026-get-closer">AI-adapted ad formats</a>, and major broadcaster integrations like <a href="https://about.netflix.com/en/news/f1-and-tf1-live-channel-now-available-on-netflix-in-france">TF1 in France</a> — media processing footprints can change unpredictably, rendering static container resource limits imprecise.</li><li><strong>Rising Industry Hardware Costs:</strong> Driven by massive global AI infrastructure demand, server hardware and DRAM prices continue to climb, making idle container memory significantly more expensive to waste.</li><li><strong>Compounding Platform Scale:</strong> At Netflix scale, even minor over-provisioning multiplies exponentially across hundreds of thousands of daily container executions, compounding wasted capacity across the entire Stratum platform.</li></ul><p>To understand the scale of the challenge, we analyzed historical usage across Stratum’s workloads. The telemetry confirmed our hypothesis: over-provisioning was frequent, wasted compute was compounding, and the sustainable path forward required a fundamental re-evaluation of how resource configurations are managed.</p><h3><strong>Meet the Stratum Resource Tuner</strong></h3><p>To address the challenges listed above, we built the Stratum Resource Tuner (SRT) to automatically right-size Stratum containers. Our north star is to ensure that users never have to manually specify the resource configurations. We started with a handful of high-throughput workloads with relaxed latency requirements, which account for the majority (70%–80%) of resource usage.</p><p>As a quick note on terminology, we use “right-sizing” and “tuning” interchangeably in this post. “Downsizing” means decreasing resource limits, while “upsizing” means increasing them. Since our goal is to optimize for cost, this post focuses on downsizing.</p><p>Automatic right-sizing is not enabled by default. Function owners must manually opt in on a per resource type basis (e.g., CPU, memory, network, or disk). Once enrolled, SRT operates without a human in the loop across five steps:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_8R96y0lKNJmgSs1qZ0qTw.png" /><figcaption><em>Figure 1: SRT Workflow High-Level</em></figcaption></figure><ol><li><strong>Collect </strong>resource usage metrics across all executions of that function.</li><li><strong>Generate </strong>a recommendation for each resource type (e.g., CPU or memory).</li><li><strong>Verify </strong>the recommendation through experiments.</li><li><strong>Apply or reject</strong> the recommendation based on the results of experiments.</li><li><strong>Monitor post-apply</strong> metrics, watching for anomalies after applying the recommendation.</li></ol><p>“Recommendation” is a key construct in SRT and serves as the primary unit of operation. As SRT executes its steps, it promotes the recommendation through several stages:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*v-4Y8rsiBponNuMHffmzuw.png" /><figcaption><em>Figure 2: Recommendation State Machine</em></figcaption></figure><ul><li><strong>CREATED</strong>: A scheduled workflow generated the recommendation.</li><li><strong>EVALUATING</strong>: Recommendation verification is running, and SRT is analyzing the results.</li><li><strong>APPLIED</strong>: All experiments passed, and SRT automatically rolled out the recommended configuration.</li><li><strong>REJECTED</strong>: At least one experiment failed, leaving the function configuration untouched.</li><li><strong>MONITORED</strong>: SRT monitors the recommendation during a post-apply soak period to verify stability.</li></ul><p>The sections below examine each of these stages in detail. To illustrate the flow, we will follow the real-world lifecycle of the <strong><em>VideoEncoder.encodeV1</em></strong> function — a high-throughput video encoding function whose over-provisioned memory and CPU footprints were successfully tuned.</p><p>Note that all operational thresholds and safety limits discussed below are fully configurable per function to accommodate varying workload characteristics and risk tolerances.</p><h3><strong>Metrics Collection</strong></h3><p>To make these recommendations possible, we need a solid metrics collection mechanism. Every Stratum container reports its function execution metadata, infrastructure dimensions, and resource utilization (CPU, memory, disk, and network) at sub-minute resolution.</p><p>These metrics flow through <a href="https://netflixtechblog.com/keystone-real-time-stream-processing-platform-a3ee651812a">Keystone</a>, Netflix’s real-time event streaming platform, and land in two separate datastores:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*w1caGsFRLs10ZW_eI4ahZw.png" /><figcaption><em>Figure 3: Metrics Collection</em></figcaption></figure><ul><li><strong>Execution summaries store:</strong> Stores execution-level summary metrics indexed in Elasticsearch for fast point lookups, serving as our short-term datastore for querying and inspecting individual runs.</li><li><strong>High-resolution long-term store:</strong> Stores raw, high-resolution usage metrics in <a href="https://netflixtechblog.com/incremental-processing-using-netflix-maestro-and-apache-iceberg-b8ba072ddeeb">Iceberg</a> tables. While slower to query, it serves as our long-term datastore, holding months of history for resource utilization trend analysis.</li></ul><p>One record is written per execution, which is represented as a span. Here is how these records look for a single span of our <strong><em>VideoEncoder.encodeV1</em></strong> function.</p><p>In Elasticsearch, the record captures the high-level summary of the completed span, identified by <strong><em>tags.stratum.spanId</em></strong>:</p><pre>{<br>  &quot;tags.stratum.spanId&quot;: &quot;38536313f65f060a&quot;,<br>  &quot;tags.functionName&quot;: &quot;VideoEncoder.encodeV1&quot;,<br>  &quot;tags.stratum.phase&quot;: &quot;run&quot;,<br>  &quot;tags.stratum.success&quot;: true,<br>  &quot;tags.stratum.retriedWithMoreResources&quot;: false,<br>  &quot;tags.stratum.function.execDurationSecs&quot;: 26,<br>  &quot;tags.cpuCount&quot;: 8,<br>  &quot;tags.memoryInMB&quot;: 18000,<br>  &quot;tags.diskInMB&quot;: 50000,<br>  &quot;tags.networkInMbps&quot;: 1000,<br>  &quot;tags.titusTaskId&quot;: &quot;06e1a7c0-2502-4a54-905f-b13a1e786107&quot;,<br>  &quot;tags.instanceType&quot;: &quot;r7a.24xlarge&quot;,<br>  &quot;tags.nf.region&quot;: &quot;us-east-1&quot;,<br>  &quot;ts&quot;: 1780743028000<br>}</pre><p>In Iceberg, the record contains the same metadata, but adds the sub-minute resource utilization timeseries tracked during that span:</p><pre>{<br>  &quot;span_id&quot;: &quot;38536313f65f060a&quot;,<br>  &quot;function_name&quot;: &quot;VideoEncoder.encodeV1&quot;,<br>  &quot;phase&quot;: &quot;run&quot;,<br>  &quot;success&quot;: true,<br>  &quot;start_epoch_secs&quot;: 1780743002,<br>  &quot;end_epoch_secs&quot;: 1780743028,<br>  &quot;runtime_secs&quot;: 26,<br>  &quot;cpu_requested&quot;: 8,<br>  &quot;memory_mb_requested&quot;: 18000,<br>  &quot;disk_mb_requested&quot;: 50000,<br>  &quot;network_mbps_requested&quot;: 1000,<br>  &quot;titusTaskId&quot;: &quot;06e1a7c0-2502-4a54-905f-b13a1e786107&quot;,<br>  &quot;instance_type&quot;: &quot;r7a.24xlarge&quot;,<br>  &quot;tags.stratum.retriedWithMoreResources&quot;: false,<br>  &quot;env&quot;: &quot;prod&quot;,<br>  &quot;resource_usage&quot;: [ <br>   { &quot;cpu_usage&quot;: 1.13, &quot;memory_mb&quot;: 3701.37, &quot;disk_mb&quot;: 20120.4, &quot;net_in_mb&quot;: 221.09, &quot;net_out_mb&quot;: 0.12 },<br>   { &quot;cpu_usage&quot;: 2.25, &quot;memory_mb&quot;: 3584.09, &quot;disk_mb&quot;: 25410.1, &quot;net_in_mb&quot;: 0.02, &quot;net_out_mb&quot;: 0.05 },<br>   { &quot;cpu_usage&quot;: 4.16, &quot;memory_mb&quot;: 6750.79, &quot;disk_mb&quot;: 30400.5, &quot;net_in_mb&quot;: 165.90, &quot;net_out_mb&quot;: 0.38 },<br>   ... <br>  ]<br>}</pre><h3><strong>Recommendation Generation</strong></h3><p>A scheduled <a href="https://netflixtechblog.medium.com/netflix-conductor-the-next-chapter-41ad21067649">Conductor</a> workflow runs daily, reading the past month of usage data from Iceberg and generating recommendations for every function and resource type. The workflow also generates recommendations for functions that have not opted into automatic right-sizing to preview potential savings.</p><p>Each recommendation is saved in the recommendations datastore, enabling us to query and filter recommendations by function, resource type, or status on an ad hoc basis.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*P7O0PYozdk5xOS3HNsOxAQ.png" /><figcaption><em>Figure 4: Recommendation Generation Sequence</em></figcaption></figure><p>For each function and resource type, the workflow applies the following generation logic:</p><ul><li><strong>Filter by workload type:</strong> We only tune high-throughput workloads. This focuses our efforts on right-sizing, which yields the most meaningful fleet-wide savings.</li><li><strong>Aggressively tune CPU and network:</strong> We target p99 usage with no extra headroom. CPU and network are compressible resources; if a container occasionally hits these limits, the function might execute slightly slower.</li><li><strong>Conservatively tune memory and disk:</strong> We target p99.9 usage and include a safety buffer. Running out of physical memory or disk space causes hard failures — such as Out of Memory (OOM) or Out of Disk (OOD) crashes — making a protective margin essential.</li><li><strong>Apply safeguards to changes:</strong> We discard recommendations if the absolute reduction is too small to justify operational effort. For memory and disk, we clamp the maximum reduction allowed in a single cycle to 15% — a safety limit introduced after learning from early production runs.</li></ul><p>The workflow creates new recommendations in the <strong><em>CREATED</em></strong> state. If a recommendation already exists in this state for a given function and resource type, SRT overwrites it with updated calculations. Once a recommendation advances past the <strong><em>CREATED</em></strong> state, its <strong><em>recommended_value</em></strong> and <strong><em>stats</em></strong> fields become immutable. Subsequent daily runs generate a new recommendation with a unique ID instead of modifying the existing record.</p><p>Here is how these recommendations look across resource types in their initial <strong><em>CREATED</em></strong> state for our <strong><em>VideoEncoder.encodeV1</em></strong> function:</p><ul><li>CPU recommendation</li></ul><pre>{<br><br>  &quot;function_name&quot;: &quot;VideoEncoder.encodeV1&quot;,<br>  &quot;env&quot;: &quot;prod&quot;, &quot;resource_type&quot;: &quot;CPU&quot;,<br>  &quot;current_value&quot;: 8, &quot;recommended_value&quot;: 4,<br>  &quot;status&quot;: &quot;CREATED&quot;,<br>  &quot;resource_cost_usd_per_hour&quot;: ***,<br>  &quot;stats&quot;: {<br>    &quot;usage&quot;: { &quot;p99&quot;: 4.16, &quot;p999&quot;: 4.57 },<br>    &quot;execution&quot;: {<br>      &quot;total_fn_executions&quot;: 748762182,<br>      &quot;success_fn_executions_pct&quot;: 99.75,<br>      &quot;total_fn_exec_runtime_hr&quot;: 88102723<br>    },<br>    &quot;window&quot;: { &quot;start&quot;: &quot;2026-05-05T00:00:00Z&quot;, &quot;end&quot;: &quot;2026-06-05T00:00:00Z&quot; }<br>  },<br>  &quot;experiment_inputs&quot;: null,<br>  &quot;experiment_workflow_execution_id&quot;: null,<br>  &quot;outcome_message&quot;: null,<br>  &quot;finalized_at&quot;: null<br>}</pre><ul><li>Memory recommendation (15% reduction in one step)</li></ul><pre>{<br>  &quot;id&quot;: &quot;2edb734a-6184-4f5e-8ffa-23004bd665d7&quot;,<br>  &quot;function_name&quot;: &quot;VideoEncoder.encodeV1&quot;,<br>  &quot;env&quot;: &quot;prod&quot;, &quot;resource_type&quot;: &quot;MEMORY_MB&quot;,<br>  &quot;current_value&quot;: 18000, &quot;recommended_value&quot;: 15300,<br>  &quot;status&quot;: &quot;CREATED&quot;,<br>  &quot;resource_cost_usd_per_hour&quot;: ***,<br><br>  &quot;stats&quot;: {<br>    &quot;usage&quot;: { &quot;avg&quot;: 5296.43, &quot;p99&quot;: 6750.8, &quot;p999&quot;: 7820.3 },<br>    &quot;execution&quot;: {<br>      &quot;total_fn_executions&quot;: 748762182,<br>      &quot;success_fn_executions_pct&quot;: 99.75,<br>      &quot;total_fn_exec_runtime_hr&quot;: 88102723<br>    },<br>    &quot;window&quot;: { &quot;start&quot;: &quot;2026-05-05T00:00:00Z&quot;, &quot;end&quot;: &quot;2026-06-05T00:00:00Z&quot; }<br>  },<br>  &quot;experiment_inputs&quot;: null,<br>  &quot;experiment_workflow_execution_id&quot;: null,<br>  &quot;outcome_message&quot;: null,<br>  &quot;finalized_at&quot;: null<br>}</pre><p>As the recommendation moves through its lifecycle, SRT populates the remaining parameters.</p><p>When proceeding to the verification stage, SRT transitions the recommendation status to <strong><em>EVALUATING</em></strong>, records the target experiment settings, and links the active workflow execution tracking the experiment.</p><p>Finally, once verification runs complete, the status changes to <strong><em>APPLIED</em></strong> or <strong><em>REJECTED</em></strong>, storing the outcome details and completion timestamp for analysis.</p><h3><strong>Retry with More Resources</strong></h3><p>We take a conservative approach to generating recommendations, prioritizing system stability over extreme efficiency. Because recommendations rely on historical usage data, they cannot always anticipate sudden spikes or novel workload profiles when functions encounter heavy, unseen requests.</p><p>To handle these unexpected spikes, we developed a fail-safe mechanism. A watcher process running in each tuned container tracks memory and disk usage during function execution. If the container exceeds the pre-set thresholds, 85% for non-cached memory usage or 95% for disk usage, the watcher flags the execution as high risk, cancels execution, and re-queues the task onto a larger container:</p><pre>new_container_size = original_container_size * 1.5</pre><p>This 1.5 multiplier only applies to memory and disk dimensions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eP3XJJoiTEqjkEu4zDyb0A.png" /><figcaption><em>Figure 5: Retry with More Resources</em></figcaption></figure><p>This “retry with more resources” mechanism serves as our safety net: larger container configurations are applied only when retrying tasks to self-heal from edge cases. Although these infrequent retries incur additional compute costs and latency, they allow us to keep day-to-day configurations highly optimized without having to size for rare, costly outliers up front.</p><p>For our <strong><em>VideoEncoder.encodeV1</em></strong> function, if a heavy run causes memory usage to exceed the 85% threshold (15,300 MB) within its current 18,000 MB container, the system aborts the execution and reruns it on a 27,000 MB container.</p><p>While we prefer to trigger retries based on actual OOM or OOD events rather than pre-set thresholds, technical constraints in our current architecture make true event detection challenging. Using pre-set thresholds serves as a reliable stopgap while we develop improved detection capabilities.</p><p>SRT automatically enables the retry mechanism for any function opted into memory or disk right-sizing. Each Stratum container reports retry events, which flow into Elasticsearch and Iceberg alongside standard performance telemetry.</p><h3><strong>Recommendation Verification</strong></h3><p>Every recommendation must pass a two-stage verification process managed by a Conductor workflow before rollout. During this <strong><em>EVALUATING</em></strong> stage, SRT executes two distinct experiment types:</p><ol><li><strong>Synthetic experiment:</strong> A low-cost, low-risk test to catch obvious regressions before production rollout.</li><li><strong>Production experiment:</strong> A live evaluation that validates changes across diverse production workloads to increase confidence.</li></ol><p>A recommendation advances to production testing only after passing the synthetic experiment. When an opted-in function has multiple pending recommendations, SRT prioritizes and evaluates the candidates with the highest estimated savings first.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*erdjwFzi3oDDxwfTZQo2og.png" /><figcaption><em>Figure 6: Evaluating Stage</em></figcaption></figure><p><strong>Synthetic experiment:</strong></p><ul><li>Runs in a test environment isolated from production traffic.</li><li>Sets up two clusters: one for the current and one for the recommended config.</li><li>Replays identical request sets across both clusters simultaneously without splitting traffic, exposing both configurations to 100% of test requests.</li><li>Typically runs for 1–2 hours.</li><li>Runs to completion without early termination checks.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Yxurwvzu6RiXUDtp8S6xHw.png" /><figcaption><em>Figure 7: Synthetic Experiment Flow</em></figcaption></figure><p><strong>Production experiment:</strong></p><ul><li>Runs in production on real user traffic, split three ways:<br>◦ ~90% remains on the main cluster (current production config).<br>◦ ~5% goes to a baseline cluster (current production config).<br>◦ ~5% goes to an experimental cluster (recommendation config).</li><li>Evaluates recommendations against a similarly sized baseline cluster rather than the main cluster to preserve comparable traffic profiles.</li><li>Typically runs from one day to one week, depending on traffic volume.</li><li>Fails fast: cancels the experiment early if regressions are detected.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AOeijZYzPgyKBjv2gHxijQ.png" /><figcaption><em>Figure 8: Production Experiment Flow</em></figcaption></figure><p>The Stratum platform executes other experiments alongside SRT experiments. To avoid over-splitting traffic, SRT experiments run at lower priority and yield resources to active, higher-priority experiments.</p><p>Traffic arrives in irregular bursts separated by multi-day or multi-week lulls. The verification pipeline accommodates these variable arrival patterns rather than expecting steady traffic.</p><p>Back to our <strong><em>VideoEncoder.encodeV1</em></strong> memory recommendation (18,000 MB → 15,300 MB), verification is triggered with:</p><pre>{<br>  &quot;recommendationId&quot;: &quot;2edb734a-6184-4f5e-8ffa-23004bd665d7&quot;,<br>  &quot;syntheticExperiment&quot;: { &quot;numberOfExecutions&quot;: 3000 },<br>  &quot;productionExperiment&quot;: {<br>    &quot;enabled&quot;: true,<br>    &quot;maxContainers&quot;: 20, // max containers shift to baseline &amp; experimental clusters<br>    &quot;upGateTimeoutMinutes&quot;: 10080, // time to wait for traffic to start flowing<br>    &quot;maxRunDurationMinutes&quot;: 1240, // max time to run the experiment once traffic starts<br>    &quot;minSamples&quot;: 500 // min executions needed on both clusters before evaluating<br>  }<br>}</pre><p>Both verification stages evaluate recommendations against two sequential checks, terminating immediately if either fails:</p><ul><li><strong>Retries with more resources:</strong> Restricts memory and disk downsizes to a maximum 1% retry threshold over the experiment duration.</li><li><strong>Execution time:</strong> Compares median runtimes to prevent outlier distortion, permitting up to a 10–15% slowdown relative to baseline, depending on workload, and is configurable by function.</li></ul><p>If a recommendation passes both verification stages, SRT updates its status to <strong><em>APPLIED</em></strong> and rolls out the configuration to the main production cluster. If a check fails at any point, SRT marks the recommendation as <strong><em>REJECTED</em></strong> and preserves the existing configuration. SRT sends notifications for both outcomes.</p><p>For <strong><em>VideoEncoder.encodeV1</em></strong>, both stages passed cleanly:</p><pre>{<br>  &quot;outcome&quot;: &quot;APPLIED&quot;,<br>  &quot;syntheticExperiment&quot;: {<br>    &quot;result&quot;: &quot;SUCCESS&quot;,<br>    &quot;samples&quot;: { &quot;baseline&quot;: 3000, &quot;experimental&quot;: 3000 },<br>    &quot;medianExecutionSecs&quot;: { &quot;baseline&quot;: 59.0, &quot;experimental&quot;: 59.0 },<br>    &quot;slownessIncreasePct&quot;: 0.0,<br>    &quot;retryRatePct&quot;: { &quot;baseline&quot;: 0.0, &quot;experimental&quot;: 0.0 }<br>  },<br>  &quot;productionExperiment&quot;: {<br>    &quot;result&quot;: &quot;SUCCESS&quot;,<br>    &quot;samples&quot;: { &quot;baseline&quot;: 50361, &quot;experimental&quot;: 49790 },<br>    &quot;medianExecutionSecs&quot;: { &quot;baseline&quot;: 196.0, &quot;experimental&quot;: 198.0 },<br>    &quot;slownessIncreasePct&quot;: 1.0,<br>    &quot;retryRatePct&quot;: { &quot;baseline&quot;: 0.0, &quot;experimental&quot;: 0.0 }<br>  }<br>}</pre><p>The recommendation moved to the <strong><em>APPLIED</em></strong> state, permanently setting the memory allocation for <strong><em>VideoEncoder.encodeV1</em></strong> to 15,300 MB. We observed similarly clean results for the CPU recommendation, which reduced CPU allocation from 8 cores to 4.</p><h3><strong>Post-Apply Monitoring</strong></h3><p>Passing verification experiments does not mark the end of the recommendation lifecycle. Updated function code, modified input parameters, or underlying changes in the compute environment (Titus/EC2) can alter production behavior over time. To ensure configuration safety and foster feature adoption, we instituted continuous post-apply monitoring for applied changes.</p><p>We monitor metrics for 90 days across two rolling time windows: a 30-minute fast window and a 24-hour slow window. Each window maintains specific alert thresholds: a 3% retry rate over 30 minutes or a 2% rate over 24 hours triggers an automated notification.</p><p>At steady state, workloads exhibit baseline retry rates around 0.1%, with rates up to 2%–3% falling within acceptable operational variances. Most applied recommendations remained stable over the past three months.</p><p><strong><em>VideoEncoder.encodeV1</em></strong> has operated on its tuned configuration for months without breaching thresholds. However, <strong><em>VideoEncoder.encodeV2</em></strong> presented a challenging edge case: a 50% memory reduction passed all verification checks and ran stably for two weeks before an unexpected traffic shift drove retry rates above 60%.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1pOE8Y1muZy4G-ldTh_RcA.png" /><figcaption><em>Figure 9: Unexpected Retries Spike</em></figcaption></figure><p>Because early versions lacked automated rollback capabilities, engineers manually triaged exception cases. This incident highlighted the need for automated mitigation, prompting us to construct self-healing rollback functionality.</p><p>Today, the two auto-revert windows look like this:</p><ul><li><strong>Last 30 minutes:</strong> 3% retries warn us; 10% triggers an auto-revert.</li><li><strong>Last 24 hours:</strong> A 2% retry rate triggers both an alert and an automatic revert.</li></ul><p>Breaching an auto-revert threshold immediately restores the function configuration to its pre-recommendation baseline.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_lgGmVYHuxqAVlll63PT7w.png" /><figcaption><em>Figure 10: Recommendation Auto-Revert</em></figcaption></figure><p>We intentionally maintain straightforward revert logic — restoring the last known stable configuration rather than attempting dynamic resource adjustments during an incident.</p><p>The <strong><em>VideoEncoder.encodeV2</em></strong> incident also reshaped our recommendation generation rules for memory and disk allocations. Rather than applying full target reductions immediately, SRT now applies smaller, incremental 15% step changes, advancing only after confirming stability at each step. Additionally, we introduced a global kill switch to disable automated right-sizing across all functions in case of unexpected issues or side effects.</p><p>Additionally, we enforce a mandatory cooldown period — typically one week — before initiating new experiments on a function. Pausing adjustments allows us to observe performance across diverse production traffic patterns, identifying potential risks before applying subsequent recommendations.</p><h3><strong>Where Are We Today</strong></h3><p>Oversized containers and config drift don’t have to be an accepted tax of running serverless workloads at scale. By building the Stratum Resource Tuner, we transformed manual, risk-averse resource sizing into an automated, self-healing pipeline. Through real-world workloads and hard lessons from unexpected spikes, we’ve proven that continuous right-sizing can safely deliver significant gains in infrastructure efficiency.</p><p>We initially focused on right-sizing high-throughput video encoding workloads, aiming for an initial 10% reduction in overall Stratum compute costs. For instance, <strong><em>VideoEncoder.encodeV1</em></strong> achieved a 50% reduction in CPU usage and 40% in memory usage without compromising performance or retry rates.</p><p>However, edge cases like <strong><em>VideoEncoder.encodeV2</em></strong> highlighted key lessons: large single-step reductions can introduce hidden risks, and manual incident response slows recovery. These insights led us to enforce stricter safety guardrails in the Stratum Resource Tuner, including capping memory and disk reductions at 15% per cycle and adding automated rollbacks when retry thresholds are breached. Guided by these safeguards, re-tuning <strong><em>VideoEncoder.encodeV2</em></strong> yielded a stable 20% memory reduction with zero subsequent incidents.</p><p>Looking ahead, our key priorities include:</p><ul><li>Expand SRT beyond high-throughput encoding workloads to support a wider variety of functions and workload types.</li><li>Further tune the resources in regions with constrained resource availability.</li><li>Move from reactive alerts to proactive, automated upsizing for under-provisioned functions.</li><li>Experiment with intentional upsizing for latency-sensitive workloads to evaluate performance gains and reduce function execution time.</li><li>Upgrade our failure signals for OOM/OOD events from lagging indicators to leading indicators to improve tuning precision.</li></ul><p>If solving problems like this sounds exciting, we are hiring in our Warsaw office. Check out open roles <a href="https://explore.jobs.netflix.net/careers?location=Warsaw%2C%20Masovian%20Voivodeship%2C%20Poland&amp;pid=790316205428&amp;domain=netflix.com&amp;sort_by=relevance&amp;triggerGoButton=true">here</a>.</p><h3>Credits</h3><p>Special thanks to <a href="https://www.linkedin.com/in/olofjohanson/">Olof Johansson</a>, <a href="https://www.linkedin.com/in/jakub-karczewski/">Jakub Karczewski</a>, <a href="https://www.linkedin.com/in/adam-mazur-51b7393/">Adam Mazur</a>, <a href="https://www.linkedin.com/in/ethan-mathieu/">Ethan Mathieu</a>, <a href="https://www.linkedin.com/in/rosanna-lee-197920/">Rosanna Lee</a>, <a href="https://www.linkedin.com/in/ameya-vasani-8904304/">Ameya Vasani</a>, Jean Barkhuysen, <a href="https://www.linkedin.com/in/phieromnimon/">Paul Hieromnimon</a>, and <a href="https://www.linkedin.com/in/adithyaprakash/">Adithya Prakash</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ddf9aa68a801" width="1" height="1" alt=""><hr><p><a href="https://netflixtechblog.com/netflix-stratum-media-processing-automated-container-right-sizing-ddf9aa68a801">Netflix Stratum Media Processing: Automated Container Right-Sizing</a> was originally published in <a href="https://netflixtechblog.com">Netflix TechBlog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Leave the Class Path in the Rearview Mirror]]></title>
            <link>https://netflixtechblog.com/leave-the-class-path-in-the-rearview-mirror-67a85b15b6be?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/67a85b15b6be</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[jvm]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[java]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Fri, 18 Sep 2026 16:01:02 GMT</pubDate>
            <atom:updated>2026-09-18T16:51:42.515Z</atom:updated>
            <content:encoded><![CDATA[<p><em>Introducing composable, module system native and agent friendly command line tools for modern Java development</em></p><p>By Danny Thomas, JVM Ecosystem Team</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/848/1*AkQlaHO9bwLf_dNp-Z2HZQ.png" /></figure><p>Recent work on the Java language to <a href="https://openjdk.org/projects/amber/design-notes/on-ramp">pave the on-ramp</a> has made it easier than ever to start a Java program and evolve it using the full language and platform. At the end of that on-ramp lies Java’s mature build and dependency management ecosystem, capable of carrying software to enormous scale and complexity.</p><p>That ecosystem reached its maturity by developing strong models for projects, dependencies, and builds. When the <a href="https://openjdk.org/jeps/261">Java Module System</a> arrived, those models were already serving developers exceptionally well. The module descriptor consequently became just another description of the project to keep in agreement.</p><p>We’re excited to announce a preview of <a href="https://github.com/Netflix/ja">ja</a> and its family of composable tools, that build on the capabilities of the Java Module System to provide a modern command line development experience for Java. We take the module descriptor and make it a complete description of a project, with dependency versions sitting naturally beside its requires directives and module metadata provided through documentation tags:</p><pre>/**<br> * @mainClass com.example.application.Main<br> */<br>module com.example.application {<br>    requires com.example.framework; // @1.2.3<br>}</pre><p>Combined with command line ergonomics you’re used to in other languages, creating and consuming Java modules has never been easier.</p><h3>Composable Tools</h3><p>Java developers have long been exceptionally well served by graphical tools. An IDE formats source, navigates between declarations and usages, presents API documentation, and maintains a compiled view of the project. That experience has been so complete that Java has had less need to expose the same capabilities through small, composable command line tools. Those gaps become quickly apparent when coding agents work with the Java language, with agents frequently struggling to locate dependencies, documentation and sources.</p><p>ja only provides command line ergonomics and tool orchestration, each feature is underpinned by a standalone tool. You don’t need to adopt ja to get the benefit of these tools, you can compose them in any way you choose:</p><ul><li><a href="https://github.com/Netflix/jig">jig</a> performs module version resolution, compilation and assembly, outputting standard module system arguments for use with other tools. It is also the bridge to and from Maven repositories providing a standalone module proxy and publishing commands</li><li><a href="https://github.com/Netflix/jfmt">jfmt</a> formats source using the <a href="https://www.oracle.com/java/technologies/javase/codeconventions-introduction.html">Code Conventions for the Java Programming Language</a>, adapted for the modern Java language. Avoids the very common whitespace, indentation, import ordering and qualified class references introduced in agent written code</li><li><a href="https://github.com/Netflix/jist">jist</a> provides source aware symbol search, providing a grep style interface for understanding class files and their associated sources. Gives coding agents access to symbols and sources without indexing, LSPs or MCPs while interoperating with other build tools via an <a href="https://github.com/Netflix/jist/wiki/Project-Integration">argument file contract</a></li><li><a href="https://github.com/Netflix/jdocserver">jdocserver</a> serves locally browsable API documentation</li></ul><p>These projects use the tool discovery and execution capabilities of the platform, and are intended to be installed in your JDK along with the standard tools. They all implement <a href="https://docs.oracle.com/en/java/javase/25/docs/api/java.compiler/javax/tools/Tool.html">Tool</a> or <a href="https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/spi/ToolProvider.html">ToolProvider</a>, allowing them to be run in process.</p><p>This is also the tool discovery and execution model for ja. There we use <a href="https://docs.oracle.com/en/java/javase/25/docs/api/java.compiler/javax/tools/OptionChecker.html">OptionChecker</a> and optional custom metadata to discover which module system options are supported so it can resolve the arguments on behalf of the tool. This provides a seamless transition from your source path modules to the standard JDK tooling such as jdeps, jlink and jshell.</p><h3>Maven as a foundation</h3><p>In a recent survey of the 1,000 most popular artifacts on Maven Central, just 232 had explicit module definitions and another 248 declared automatic module names. The remaining 520 expressed no Java module name opinion. The module system also makes no distinction between namespace and module name, so module-first tooling requires a solution to module naming and location in existing repositories.</p><p>Fortunately, Maven Central already gives published artifacts a verified namespace. Publishers prove control of reverse domain group IDs, reflecting Sonatype’s long standing case for <a href="https://www.sonatype.com/blog/why-namespacing-matters-in-public-open-source-repositories">namespaces in public repositories</a>.</p><p>We use these conventions to establish a canonical Maven module coordinate, paring a verifiable DNS namespace with the complete module name, for example pkg:maven/com.netflix/com.netflix.tools.ja. For existing modules, authors choose to publish a single Maven relocation pom at the canonical coordinate, to allow for discovery of the original coordinate.</p><p>When neither are available, candidates are walked from the root of the namespace using common Maven artifact conventions inferring coordinates from module names. We also bundle a short list of aliases for the most popular modules that don’t use a reverse DNS module name, but we suggest authors should always <a href="https://blog.joda.org/2017/04/java-se-9-jpms-module-naming.html">namespace their modules</a>. The module proxy in jig presents resolved modules using the filename based conventions for module naming, making even automatic modules without stable names safe when used with these tools.</p><p>These conventions and location strategies allow the majority of existing artifacts to be discovered using only the module name and version.</p><h3>Integrity by default</h3><p>ALL-UNNAMED has become unfortunately common in Java access options, because of the heavy use of the class path. It hides the source of the technical debt that applications are incurring by allowing such access and becomes increasingly consequential as Java moves toward <a href="https://openjdk.org/jeps/8305968">Integrity by Default</a>. For example, <a href="https://openjdk.org/jeps/500">Preparing to Make Final Mean Final</a> asks applications to explicitly authorize the modules allowed to mutate final fields.</p><p>We allow runtime access requirements to bedeclared as module metadata and carried with the module descriptor throughout the module’s lifecycle. For example a library may record the access it requires:</p><pre>/**<br>* @enableFinalFieldMutation com.example.framework<br>*/<br>module com.example.framework {<br>}</pre><p>However, the consuming application remains in control and must explicitly authorize the framework, for it to be available at runtime:</p><pre>/**<br>* @mainClass com.example.application.Main<br>* @enableFinalFieldMutation com.example.framework<br>*/<br>module com.example.application {<br>    requires com.example.framework; // @1.2.3<br>}</pre><p>The command line interface for ja allows the dependency and authorization to be added together:</p><pre>ja require com.example.framework@1.2.3 \<br> --enable-final-field-mutation com.example.framework</pre><p>Without that authorization, dependency resolution fails with an unsatisfied access requirement. Native access follows the same model through @enableNativeAccess and qualified exports and opens are also supported.</p><p>Module integrity is ensured by persistent hashes of resolved binary dependencies in a module-info.hash file, sequent resolution verifies those hashes and rejects an artifact that has changed.</p><p>We also take a step further than the recent improvements to annotation processor security by treating annotation processing as an explicit code generation step. The resulting sources are alongside regular module source, making them visible in code review and allowing a module to be assembled without executing generator code.</p><h3>Make modules your default</h3><p>We think every Java project should be modular, regardless of the build tool you’re using. If you’re a library author producing automatic modules, we’d encourage you to avoid split packages and produce explicit modules.</p><p>You can get started with our tools today with our <a href="https://github.com/Netflix/ja">installation guide</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=67a85b15b6be" width="1" height="1" alt=""><hr><p><a href="https://netflixtechblog.com/leave-the-class-path-in-the-rearview-mirror-67a85b15b6be">Leave the Class Path in the Rearview Mirror</a> was originally published in <a href="https://netflixtechblog.com">Netflix TechBlog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Lifecycle of LLM-as-a-Judge: Building, Aligning, and Monitoring at scale]]></title>
            <link>https://netflixtechblog.com/the-lifecycle-of-llm-as-a-judge-building-aligning-and-monitoring-at-scale-c95bd8283508?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/c95bd8283508</guid>
            <category><![CDATA[human-in-the-loop]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[llm-as-a-judge]]></category>
            <category><![CDATA[recommendation-system]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Fri, 04 Sep 2026 16:01:02 GMT</pubDate>
            <atom:updated>2026-10-14T19:48:51.292Z</atom:updated>
            <content:encoded><![CDATA[<p><em>By </em><a href="https://www.linkedin.com/in/emma-yanyang-kong-6904b457/"><em>Emma Yanyang Kong</em></a><em>, </em><a href="https://www.linkedin.com/in/jj-tan-code/"><em>JJ Tan</em></a><em>, </em><a href="https://www.linkedin.com/in/ishangupta1993/"><em>Ishan Gupta</em></a><em>, </em><a href="https://www.linkedin.com/in/gareth-lars-olds-62b01068/"><em>Lars Olds</em></a><em>, </em><a href="https://www.linkedin.com/in/claire-campbell-65a06727/"><em>Claire Campbell</em></a><em>, </em><a href="https://www.linkedin.com/in/dfagnan/"><em>David Fagnan</em></a><em>, </em><a href="https://www.linkedin.com/in/rkavuri/"><em>Ratna Kavuri</em></a>, <a href="https://www.linkedin.com/in/rohangosain/"><em>Rohan Gosain</em></a><em>, </em><a href="https://www.linkedin.com/in/veli-balin-54a74516/"><em>Veli Balin</em></a>, <a href="https://www.linkedin.com/in/givenday/"><em>Minsu Jang</em></a><em>, </em><a href="https://www.linkedin.com/in/louis-garcia-81836467/"><em>Louis Garcia</em></a></p><h3><strong>Introduction</strong></h3><p>Any team generating text with an LLM at scale hits the same problem. You cannot evaluate everything manually. You don&#39;t know which outputs are great, which are mediocre, and which are harmful, because you simply cannot read them all. The standard solution is <strong>LLM-as-a-Judge</strong>, a second model that scores the first model’s output as a proxy for human raters. It is fast, cheap, and easy to deploy. However, it introduces a new and critical question: <em>Who evaluates the evaluator?</em> Furthermore, if the judge drifts, quietly degrading and starting to approve bad output, nothing downstream will flag it.</p><p>This post is about how we solve that problem, by building, aligning, and continuously monitoring LLM judges so they stay trustworthy over time. Our core principle is simple: <strong>An LLM judge must never operate alone. It has to be anchored in human judgment. </strong>We keep a <strong>human-in-the-loop (HITL)</strong> at every phase of the lifecycle. Humans define what “good” means, create the labeled benchmark datasets to train and validate the judge, monitor its behavior while it runs in the system, and review and approve any updates before they’re rolled out.</p><p>We believe:</p><p><strong>A reliable LLM judge is not a static artifact. It has a lifecycle.</strong></p><p>It needs to be <em>built</em> against trusted human labels, <em>aligned</em> with human preference and reasoning, <em>run</em> under human-set policy, and <em>maintained</em> as the data and usage pattern shift. Each of these stages has human owners. In the sections below, we walk through these four phases, the key design decisions in each, and the lessons we’d share with a team building their own LLM judge, using one concrete task as our running example: recommendation explanations.</p><p>A <strong>recommendation explanation</strong> is a short natural-language message that accompanies a recommended item, and it shapes how users perceive and trust a recommender system. In our work, an explanation is human-readable evidence for <em>why</em> a title was recommended based on what the member has previously watched, shown on the title’s detail page, which is the screen a member reaches by selecting a title from the homepage. We focus on <em>similarity-based</em> explanations, which connect a recommended title to one reference title the member has interacted with via shared attributes such as genre and tone. For example,</p><p><em>A funny, heartfelt holiday romance about love and new beginnings, much like “My Secret Santa.”</em></p><p>Quality matters here because a misleading or poorly grounded explanation erodes the very trust the explanation is meant to build.</p><p>Everything that follows comes out of a series of controlled, user-facing online experiments we ran on the explanation feature, including a month-long experiment on the Netflix mobile app. In these tests we used an LLM to generate explanations, hundreds of thousands a week across a constantly changing catalog. Manually reviewing them at this scale isn’t feasible, so we deployed a second LLM to evaluate them.</p><p>What counts as a good explanation is specific to our task. However, the way we build, align, and monitor the LLM judge for evaluating explanations should be reusable and generalizable across different tasks.</p><h3><strong>The Lifecycle at a Glance</strong></h3><p>Figure 1 summarizes the four phases of our LLM-judge lifecycle and the data flow connecting them.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hJdG1aeP8E3Hil6mJwcp1Q.png" /><figcaption><em>Figure 1. The four-phase lifecycle of an LLM judge, and the data that flows between the phases.</em></figcaption></figure><p>We start with <strong>Birth: Benchmark Data Creation</strong>, where human experts craft hard or near-boundary examples and human raters label them with pass/fail decisions and written rationales. We augment this with LLM-generated gray-area examples to build a richer benchmark dataset.</p><p>Next, in the second phase of <strong>Training: LLM Judge Development</strong>, we tune the LLM judge to align with human labels and rationales from the Birth phase, using self-reflection to identify mismatches between the judge and humans, and updating the rubrics based on those mismatches and anchor examples. We iterate in this phase until our metrics show that the judge is sufficiently aligned with human raters.</p><p>In the third phase, <strong>Deployment</strong>, we integrate the judge into the explanation generation system as both a gate and a critic. The LLM generator writes explanations, and the LLM judge scores them and either passes them through or blocks them and supplies a rationale that the generator can use for self-refinement and retries. Passed explanations are served to members, and low-quality explanations are gated.</p><p>Finally, in the last phase, <strong>HITL Alignment &amp; Drift Monitoring</strong>, we keep the judge aligned with humans over time. “Human-in-the-loop” here means a person is directly involved in the decision, not just reviewing outcomes after the fact. We benchmark the judge against trained raters, and a human approves every change before it is rolled out to the system. During each test we sampled a set of explanations each week (both passed and gated, with a bias toward newer items), had human raters review them with labels and rationales, and compared the judge against these human ratings. If drift exceeded a threshold, we triggered retraining and benchmark data augmentation. This closes the loop by feeding new human-labeled data and updated signals back into the benchmark and development phases.</p><h4><strong>System components</strong></h4><p>Four automated components appear across all phases, alongside the human roles that define, validate, and supervise them.</p><ul><li><strong>Generator: </strong>produces the explanation text, optionally using prior judge rationales and traces as part of its prompt for revision.</li><li><strong>Judge: </strong>evaluates an explanation and returns a pass/fail label together with a written rationale. In the deployed pipeline, it acts as both a guardrail (gating bad outputs) and a critic (providing feedback for self-refinement).</li><li><strong>Reflector:</strong> revises the Judge’s rubric when the Judge makes mistakes. It updates the rubric based on mismatched examples, human rationales, and anchor examples. It tunes the rubric and does not directly evaluate explanations.</li><li><strong>Meta-Judge: </strong>checks whether the Judge failed an explanation for the <em>right reason</em> by comparing the Judge’s rationale against a human rater’s rationale.</li><li><strong>Human experts and raters: </strong>the experts who define the quality criteria; the raters whose labels and written rationales the Judge is tuned and measured against; the reviewers who monitor the Judge while it is running and explicitly approve any changes before they were rolled out.</li></ul><h3><strong>Phase I, Birth: Establishing Ground Truth</strong></h3><p>Everything downstream depends on knowing what “good” means, so this is the most human-intensive phase. Our internal writing experts define the must-have criteria an explanation must satisfy, author detailed labeling guidelines, and hand-craft tricky adversarial examples. Those same guidelines do double duty. They’re both the rulebook for human raters <em>and</em> the seed rubric for the LLM judges we tune later.</p><h4><strong>Why we built our own benchmark</strong></h4><p>Off-the-shelf LLM-judge benchmarks focus on general-purpose text evaluation and miss what makes our task specific. Explanations are short, item-specific, and context-dependent (each one is tied to a target title and one or two reference titles the member has watched). We also care about alignment to our own editorial bar, not just generic language quality. As a result, we built our own custom benchmark from three complementary sources:</p><ul><li><strong>Expert-crafted adversarial cases: </strong>hand-authored pass/fail examples with known failure modes, the kind of edge cases nothing else reliably produces.</li><li><strong>Boundary examples: </strong>LLM-synthesized explanations that are deliberately pushed near the pass/fail boundary, then rated by humans. These capture the ambiguous cases that sampling from live traffic rarely surfaces on its own.</li><li><strong>Real sampled explanations: </strong>explanations sampled from the output of our LLM generator during test runs.</li></ul><h4><strong>Two details that mattered</strong></h4><ul><li><strong>The benchmark is not frozen. </strong>A<strong> </strong>monitoring pipeline (Phase IV) appends a fresh batch of human-rated explanations on a weekly cadence, so over the course of a test the benchmark tracked the current catalog and new usage patterns instead of going stale.</li><li><strong>Every “fail” label carries a free-text rationale </strong>explaining <em>why</em> the explanation failed. This looks like a small bookkeeping detail, but it is actually critical for the next phase, since those rationales are what we use to tune and audit the judge’s reasoning.</li></ul><h3><strong>Phase II, Training: Aligning the Judge to Humans</strong></h3><h4>The failure mode: right answer, wrong reason</h4><p>There is a failure mode that does not show up in label-agreement metrics at all. A judge can agree with humans on the <em>label</em>, passing when they would pass and failing when they would fail, while reaching the right verdict for the <em>wrong reason</em>. On a dashboard it looks perfectly aligned. But as soon as you ask it to <em>explain</em> a rejection, which our pipeline does to steer self-revision, it can return misleading guidance.</p><p>We therefore care about not just label agreement but also <strong>reasoning alignment</strong>. In our setup, the Judge is always asked to output:</p><ul><li>a <strong>label</strong> (e.g., PASS/FAIL), and</li><li>a short <strong>natural-language rationale</strong> for that label.</li></ul><p>Here is an illustrative example. The member watched a musical, a family fantasy with a young heroine and a well-known ensemble cast. We recommend an adventure movie that is also a family fantasy with a young heroine, which is why the recommender paired them. But it shares neither the songs nor the ensemble cast.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yyJxi8QanR8YiA3JA6qySQ.png" /></figure><p>The label (FAIL) is correct, and the stated reason is reasonable at first glance. But it turns out to be incomplete in a way that simple label agreement cannot detect, as we show below.</p><h4><strong>Reasoning-Aligned Rubric Tuning</strong></h4><p>We tune each criterion’s LLM Judge with a procedure we call <strong>Reasoning-Aligned Rubric Tuning (RART)</strong>. As introduced in the system components above, the Judge, Meta-Judge, and Reflector are distinct components. The Judge scores an explanation against a rubric and gives a rationale, the Meta-Judge compares that rationale against a human-written one, and the Reflector edits the rubric text itself. RART is how the three are combined into a training loop.</p><p>Every Judge is a fixed prompt template parameterized by a <em>rubric</em>, so tuning it does not mean retraining the underlying model. Instead we iteratively refine the <em>rubric text</em> until the Judge’s behavior matches human labels <em>and</em> human rationales. At iteration <em>i</em>, the Judge is conditioned on the current rubric text and maps an explanation to a pair, a label and a rationale.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/400/1*3BQS5OYjSUnn6bzrwdRFcA.png" /><figcaption>R_i is the current rubric text, y_J(x) the label, and r_J(x) the rationale.</figcaption></figure><p>At a high level, each RART iteration proceeds as follows.</p><ol><li>Score the training data with the current rubric.</li><li>Evaluate weighted alignment metrics on a held-out validation set. If all per-criterion metrics exceed their thresholds, stop.</li><li>Otherwise, construct an error-driven <em>focus set</em> of misaligned examples and hand it to the Reflector, which proposes a revised rubric.</li></ol><p>The key difference from a standard loop is how we define a “mistake.” Most self-improvement loops look only at label disagreements. RART adds a second class of error. The Meta-Judge examines the cases where the Judge and a human <em>both s</em>aid fail and compares the Judge’s stated reason against the human’s rationale. When the two diverge, that is a <em>right answer, wrong reason </em>error, and it is also fed to the Reflector. Concretely, the focus set handed to the Reflector is the union of two error classes:</p><ul><li><strong>Label mismatches: </strong>examples where the Judge’s label differs from the human’s.</li><li><strong>Right label, wrong reason: </strong>examples where the Judge and the human both said <strong>FAIL</strong>, but the Meta-Judge finds their two rationales don’t match.</li></ul><p>So the Reflector revises the rubric from <em>both</em> ordinary label errors and <em>right label, wrong reason</em> errors. That second term is the core of RART. The Judge is pushed not only to match human decisions but to justify them in a way the Meta-Judge recognizes as aligned with human reasoning.</p><p>Returning to the earlier example, the LLM Judge said FAIL and a human said FAIL, so a label-only check would register agreement. Placing the two reasons side by side shows what that check misses.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*k7cFHDQda-pdS29GSJQkDQ.png" /></figure><p>RART routes it into the focus set, so the next rubric revision has to account for it.</p><p>Since the Meta-Judge is an LLM too, we checked it against humans first. Trained human raters labeled a sample of agreed-fail rationale pairs, and the Meta-Judge matched their verdicts on nearly all of them.</p><h4><strong>Alignment metrics</strong></h4><p>We run one Judge per criterion, each with its own rubric, tuned and scored on its own. So everything below is measured <em>per criterion</em> on held-out data. For each one we track three metrics.</p><ul><li><strong>Specificity (fail-recall): </strong>how often the Judge correctly rejects bad explanations. This is the one we care about most, because a bad explanation that slips through the gate is a trust hazard.</li><li><strong>Recall (pass-recall): </strong>how often it correctly passes good explanations, which keeps coverage high and revision cost low.</li><li><strong>Reasoning agreement: </strong>on the explanations both reject, how often they reject for the <em>same</em> reason.</li></ul><p>Each criterion’s Judge gets its own three, and one can be healthy while another slips. That is also why the gate is strict. An explanation has to pass <em>every</em> must-have criterion to be eligible to serve.</p><h4><strong>Ablation: does the reasoning signal help?</strong></h4><p>To isolate the impact of the reasoning signal, we compare RART to an otherwise identical loop that uses only <em>label</em> disagreements. We call this baseline <em>vanilla</em>: same Reflector, same Judge, same data, but no Meta-Judge and no use of human rationales.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wBff72fBvle8jRevgDO-Pg.png" /><figcaption><em>Figure 2. Improvement over the starting rubric, per criterion, for reasoning-aware tuning (RART) and label-only tuning (vanilla).</em></figcaption></figure><p>Figure 2 shows the result. On two of the three must-have criteria, vanilla and RART land in the same place, because the starting rubric was already near ceiling and the metrics had very little room to gain. On the third criterion, the vanilla (label-only) method degrades. The Reflector in the vanilla loop rewrites the rubric into degenerate forms, and specificity and reasoning agreement fall with every iteration, ending up worse than the rubric it started from. RART stays stable on that same criterion and lifts both, with a small recall gain as well. The reason is that a vanilla (label-only) optimizer can reach the right label through unsound reasoning, and unsound reasoning doesn’t generalize to new data. Anchoring the signal on the rationale prevents that. Reasoning alignment helps where there is headroom and doesn’t hurt where there isn’t.</p><h3><strong>Phase III, Deployment: Putting the Judge to Work</strong></h3><p>Once deployed, the judge sat inside the generation pipeline, gating and critiquing explanations as they were produced during our tests.</p><h4><strong>Two roles for one judge</strong></h4><p>Once we align the LLM Judge with humans, we reuse it in two distinct roles for every generated explanation:</p><ul><li><strong>Role 1, the gate. </strong>The Judge scores each explanation against the must-have criteria and rejects anything that fails.</li><li><strong>Role 2, the critic.</strong> When an explanation fails, we don’t just throw it away. The Judge’s free-text reason gets appended to the Generator’s prompt, and the Generator tries again.</li></ul><p>The second role turns the pipeline into a <em>generate → judge → revise </em>loop, with the Judge’s critique steering each revision.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IN0IUt35GmCQNDejHJ2YkA.png" /></figure><h4><strong>Bounded retries, and why we drop rather than serve</strong></h4><p>We cap the retries. Each revision costs another round of generation and judging, and the gains flatten quickly. Figure 3 shows what the loop actually buys, on 1,000 generated explanations run through as many as 12 revisions with four generators. Every curve climbs, which is the Judge’s critique doing its job, but the steep part is early, and the gap between generators is obvious. Revision <strong>amplifies a capable generator rather than rescuing a weak one</strong>. In our test runs we used a budget of <strong>3 retries </strong>with our strongest generator, and an explanation still failing after that was <em>dropped</em> rather than served.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/953/1*_qvOssxJ-Vrt8FqfghzMNg.png" /><figcaption><em>Figure 3. Cumulative share of explanations that clear the Judge as we allow more revisions, across four generator models.</em></figcaption></figure><p>That “drop rather than serve” rule is deliberate. The two outcomes aren’t symmetric. A bad explanation is a trust hazard, while a missing explanation just forgoes an opportunity. When in doubt we’d rather show nothing, and most explanations cleared the bar within the retry budget.</p><h4><strong>Do judge-approved explanations help members?</strong></h4><p>Passing the Judge proves an explanation is <em>good</em> by our standards, but it doesn’t prove that judge-approved explanations are <em>useful</em> to members. To test that connection, we ran a one-time controlled experiment on the Netflix mobile app, explanations vs. no explanations on the title detail page, over roughly a month, with the pipeline generating and judging hundreds of thousands of explanations a week during the period.</p><p>Members who saw explanations shifted their viewing toward titles they hadn’t watched before, and more of their browse sessions ended in a meaningful play. Both effects were statistically significant, and we saw no member-initiated takedowns or escalations about explanation quality during the test. The result worth carrying over isn’t the size of that effect. It’s that offline agreement with our raters was <em>necessary</em> for these explanations to work but never <em>sufficient</em> to show they help members. Only the online A/B test could establish that judge-aligned explanations are not just quality-compliant but genuinely useful.</p><h3><strong>Phase IV, Monitoring: Keeping the Judge Aligned</strong></h3><p>A judge that’s well aligned at launch won’t stay that way. The catalog shifts, and so does what counts as high quality. The lifecycle has to close the loop.</p><h4><strong>The weekly human review</strong></h4><p>For each week a test was running, we pulled roughly <strong>300 generated explanations </strong>for human review, stratified across the Judge’s decisions (<em>served</em>, <em>revised</em>, <em>dropped</em>) and weighted toward <strong>newer catalog items</strong>, where drift is most likely. The stratification stayed fixed week to week, so successive weeks were comparable. A panel of at least three raters reviewed each explanation, and the majority label was our ground truth. Those labels did two jobs. They told us whether judge-human agreement was slipping, and they grew the benchmark.</p><h4><strong>Setting the bar with human disagreement</strong></h4><p>The panel also tells us how much the raters disagreed <em>with each other</em>, which turns out to be the right yardstick for the Judge. Rather than hold it to a fixed threshold, we score the Judge and each rater against the <strong>same</strong> <strong>majority label</strong> and ask that the Judge fall within the raters’ own spread.</p><p>Take one criterion and one of its metrics, M. That week’s sample gives one value M(J) for the Judge and a set of per-rater values M(H) for the three or more raters on the panel. We require</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/452/1*rixBBMfnCWN80rxwyStDsA.png" /></figure><p>so the Judge must score no worse than two standard deviations below the average rater. A harder week widens human disagreement and widens the band with it, so the Judge isn’t penalized for difficulty the humans also felt.</p><p>Falling below the band on any metric-criterion pair is a <strong>drift event</strong>, which triggers re-tuning (back to Phase II) on the augmented benchmark. A re-tuned rubric never goes live automatically. Someone on the team reads the proposed changes and signs off before it reaches the running pipeline, and we keep the previous rubric for instant rollback.</p><h4><strong>What human review catches that metrics can’t</strong></h4><p>Beyond asking whether the Judge still agrees with human raters, the weekly review answers a second question. Are the current guidelines still asking for the right things?</p><p>In the weekly review process, reviewers surfaced issues that did not show up in Judge–rater agreement. An explanation can use accurate attributes and still overstate how similar two titles really are. A single vague adjective (“exciting”, “fun”, “emotional”) can technically satisfy the rubric while saying almost nothing. Sometimes the reference itself is the problem, something the member wouldn’t think of as a comparable title at all, so the comparison lands oddly no matter how the attributes line up. Some genres show the same problem at scale. Stand-up specials can share similar metadata tags but differ completely in tone and cultural context, so a comparison may look justified on paper, not be bad enough for a rater to fail, and still land poorly with members. And some explanations meet every must-have criterion while still reading like generic “word soup” that does not help the user much.</p><p>When recurring patterns kept appearing, we updated the Phase I guidelines, which seed both the raters’ instructions and the judges’ rubrics, so one revision updates both sides. Other times we added product rules outside the Judge, for example by blocking certain title types from being used as references.</p><p>Drift detection catches a judge that has stopped agreeing with people. Only the continued human review tells us when our <em>definition</em> of a “good explanation” is too narrow or incomplete. <strong>We need both.</strong></p><ul><li>Drift detection keeps the Judge aligned with the current rubric.</li><li>Continuous human review keeps the rubric aligned with what actually works for members.</li></ul><h3><strong>What We Learned</strong></h3><p>Five lessons we think transfer to any team shipping an LLM judge:</p><ol><li><strong>Invest in the benchmark before the judge.</strong> A modest, <em>rationale-annotated</em> benchmark beats a large label-only one. Those written rationales are exactly what make reasoning-aligned tuning possible. You can’t correct a judge’s reasoning if you never wrote down the human’s.</li><li><strong>Tune on reasoning, not just labels. </strong>A judge can reach the right verdict for the wrong reason, which looks fine on a dashboard but gives misleading feedback and doesn’t hold up on new data. In our ablation, the label-only loop degraded a rubric that the reasoning-aware loop kept stable.</li><li><strong>One aligned judge can serve many roles.</strong> Reusing the same judge as both gate and critic amortizes the alignment cost and keeps production behavior consistent across roles. Alignment is the expensive part, so get one judge right and use it more than once.</li><li><strong>Plan for monitoring from day one.</strong> A recommender platform’s items and members never stop changing, so an LLM judge, like any ML model, has to be watched and re-tuned if needed, not shipped and forgotten.</li><li><strong>Keep people in the loop by design, not by exception.</strong> Automating evaluation is not the same as delegating responsibility for it. In our lifecycle, humans set the standard (Phase I), supply the rationales the judge is aligned to (Phase II), decide the conservative default of dropping rather than serving a doubtful output (Phase III), and review a standing sample of live decisions and approve every rubric change before it ships (Phase IV). These checkpoints are much harder to retrofit once a judge is already making decisions on its own. The standing review also tells us what our rubric is missing, not just when our judge has slipped.</li></ol><p>Taken together, the judge extends the reach of our reviewers, and those reviewers are the basis for trusting the judge. That relationship is what makes it responsible to run this system at scale. An automated evaluator that no one audits provides no assurance about the decisions it is making.</p><h3><strong>Acknowledgements</strong></h3><p>This post summarizes our research paper, “<a href="https://arxiv.org/pdf/2608.18300">The Lifecycle of LLM-as-a-Judge for Large-Scale Recommendation Explanations,</a>” which has the full methodology, metrics, and ablations.</p><p>Our thanks to the internal human writers and review experts who defined what a good explanation is and keep those guidelines current; to our <strong>Human Data Operations</strong> team and the raters they train and calibrate, whose labels and written rationales are what the judges are aligned to; and to the engineers across the team who built and operate the pipeline; and to our UI engineering partners, who built the member-facing surfaces that bring these explanations to the homepage.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c95bd8283508" width="1" height="1" alt=""><hr><p><a href="https://netflixtechblog.com/the-lifecycle-of-llm-as-a-judge-building-aligning-and-monitoring-at-scale-c95bd8283508">The Lifecycle of LLM-as-a-Judge: Building, Aligning, and Monitoring at scale</a> was originally published in <a href="https://netflixtechblog.com">Netflix TechBlog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>