<?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>Wed, 17 Jun 2026 15:58:06 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[A Human-Augmenting Agentic Workflow for Causal Inference]]></title>
            <link>https://netflixtechblog.medium.com/a-human-augmenting-agentic-workflow-for-causal-inference-4623f0a9c5af?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/4623f0a9c5af</guid>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[agentic-workflow]]></category>
            <category><![CDATA[causal-inference]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Mon, 08 Jun 2026 15:01:03 GMT</pubDate>
            <atom:updated>2026-06-08T15:01:03.094Z</atom:updated>
            <content:encoded><![CDATA[<p>By Winston Chou, Adrien Alexandre, Lars Olds, Yi Zhang, Garrett Hagemann, and Nathan Kallus</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7midjxNL3H3BlZvQy4seSA.png" /></figure><h3>Introduction</h3><p>Imagine asking a data agent to analyze the causal relationship between two variables, such as the effect of watching a popular Netflix show on long-term member retention. It queries your data, runs a regression, and confidently returns an answer. How much should you trust it? Can you be confident that the agent accounted for subtle biases — or does it treat passionate fans as if they were the average viewer? Without deep understanding and expertise, would you even be able to tell if it got the answer wrong?</p><p>Data analysis is increasingly being delegated to software agents. While this reduces human effort and toil, oversight is still needed to ensure the validity of results. This is especially true for specialized tasks like <a href="https://en.wikipedia.org/wiki/Observational_study">Observational Causal Inference (OCI)</a>, which require substantial judgment and domain expertise.</p><p>In this blog post, we share an agentic workflow for performing OCI under <a href="https://www.nber.org/system/files/working_papers/t0294/t0294.pdf">unconfoundedness</a>. Our workflow is designed for software agents to adhere to rigorous, exhaustive templates for causal inference tasks. Yet, it also seeks to be “<a href="https://pubsonline.informs.org/doi/10.1287/mnsc.2024.05684">human-augmenting</a>,” and to enable and empower human inspection and evaluation.</p><p>We designed this workflow with OCI practitioners in mind. Although OCI requires context and care to do well, aspects of it — e.g., checking and rechecking covariate balance, conducting sensitivity analyses, and keeping track of multiple iterations — can be repetitive and prone to error. Our workflow seeks to eliminate this toil so that humans can focus on more nuanced tasks, such as framing questions, scrutinizing assumptions, and evaluating results.</p><p>To this end, <strong>we are open-sourcing a standalone version of our </strong><a href="https://github.com/Netflix-Skunkworks/oci-agent"><strong>oci-agent</strong></a> so that OCI practitioners can model workflows on and suggest improvements to it. We also share evaluations of our agent on the 2016 Atlantic Causal Inference Conference (ACIC) <a href="https://files.eric.ed.gov/fulltext/ED591944.pdf">competition datasets</a>, and show that our agent systematically beats one-shot iterations under numerous data-generating processes — while achieving competitive results against hand-tuned benchmarks.</p><p>This post describes the principles behind our workflow and gives a case study of its deployment at Netflix.</p><h3>Philosophy</h3><p>Our workflow is built on top of Netflix’s pre-existing OCI toolkit. We built this toolkit — largely in a pre-AI world — to answer “point-in-time” causal questions, such as “what is the effect of playing a Netflix game on member retention?” or “what is the effect of watching a highly popular show on subsequent engagement?” Questions of this kind inform business strategy, guide metric development, and contribute to a rich understanding of what drives member satisfaction.</p><p>Our toolkit is guided by a “<a href="https://academic.oup.com/aje/article-abstract/183/8/758/1739860">target trial emulation</a>” philosophy. For any point-in-time OCI question, we ask “what is the ideal A/B test for addressing this question?” This A/B test may be expensive, slow, or even infeasible to run. However, the thought exercise helps to pin down the assumptions needed for a credible answer, such as unconfoundedness of the treatment.</p><p>To make the target trial analogy actionable, our toolkit embeds a series of <strong>design diagnostics</strong>. These diagnostics assess whether we are drawing fair comparisons between treated and untreated units — or if there are hidden differences that could imperil our conclusions:</p><ul><li><em>Covariate balance.</em> After weighting, the standardized mean difference of pre-treatment covariates between treatment and control groups should be less than 0.2.</li><li><em>Overlap.</em> The probability of receiving treatment (aka propensity score) should be bounded between 0.1 and 0.9.</li><li><em>Placebo outcome.</em> The “treatment effect” on variables measured prior to the treatment should not be significantly different from zero.</li><li><em>Sensitivity to hidden confounders.</em> Findings of treatment effects are contextualized by sensitivity to hypothetical omitted variables that explain both treatment and outcome.</li></ul><p>As we uplevel our OCI toolkit with agents, such evaluation remains paramount. The standard approach to evaluating agents is to programmatically compare their outputs to ground truth. Yet, outside of artificially simulated data, there is <a href="https://en.wikipedia.org/wiki/Rubin_causal_model#The_fundamental_problem_of_causal_inference">no ground truth in observational causal inference</a>.</p><p>Without discounting the need for evals (which our workflow also supports), one of our key principles is to augment <em>human</em> evaluation by making each analytic step as transparent as possible. For example, in our workflow, agents publish artifacts in the form of plans, specifications, plots, and notebooks that humans can inspect and re-execute if they wish. In the absence of ground truth, we rely on these “process audits” — coupled with human oversight — to build good agents.</p><h3>Principles</h3><p>Our workflow has three key personas:</p><ol><li><strong>Principal</strong> — the human user (e.g., data scientist) whose goal is to provide a thorough and correct analysis</li><li><strong>Actor</strong> — the software persona that performs the analysis, including diagnostics</li><li><strong>Critic</strong> — the software persona that synthesizes results, identifies gaps, and offers suggestions to improve the analysis</li></ol><p>Our agent orchestrates the latter two personas in an actor-critic loop: specifying and triggering the analysis as the actor, then interpreting results and diagnosing flaws as the critic.</p><p>Each persona has responsibilities:</p><p><strong>Principals</strong></p><ul><li>Provide an initial analysis plan containing its context and goals.</li><li>Provide context on the main threats to valid inference and the confounders that must be controlled.</li><li>Specify the tools that can be used for the analysis.</li><li>Specify the data model and dataset.</li></ul><p><strong>Actors</strong></p><ul><li>Refine the principal’s plan into a data analysis spec.</li><li>Use only the tools provided by the principal.</li><li>Create human- and machine-checkable artifacts.</li><li>Perform the four design diagnostics in addition to the core analysis.</li><li>Report any remediations taken in case of diagnostic failures.</li></ul><p><strong>Critics</strong></p><ul><li>Check for blind spots, such as unmentioned confounders, in the principal’s plan.</li><li>Check for alignment between the plan, spec, and executed analysis.</li><li>Specify a credibility level in the results after seeing the diagnostics.</li><li>Specify if and how the estimand differs from the Average Treatment Effect (ATE), for example due to propensity score trimming.</li><li>Contrast the executed analysis with the ideal target Randomized Controlled Trial (RCT).</li><li>Suggest at least one alternative measurement strategy (e.g., encouragement RCTs).</li></ul><p>Although our workflow is designed for OCI under unconfoundedness, the principles listed in this section are meant to be extensible to other approaches to OCI, such as panel methods with very different assumptions (e.g., parallel trends).</p><h3>Empowering Human Evaluation</h3><p>To empower human oversight of each analytic step, we provide principals with a templated notebook that uses our vetted (non-agentic) OCI toolkit, which employs <a href="https://arxiv.org/abs/2506.07462">doubly robust learning for causal effect estimation</a>.</p><p>The principal’s remaining responsibilities are to write the initial analysis plan and to evaluate the analysis artifacts (the executed notebook and the critic’s report). To enable thorough evaluation, agents version-control their reports and upload executed notebooks to a file store, where they can be downloaded and re-executed by principals (if they wish).</p><p>We diagram this workflow below:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1cTcjXY_Gh80OB2x1xzlAQ.png" /></figure><h3>Case Study — Estimating the Impact of New Entertainment Types</h3><p>In recent years, we have added a wide variety of entertainment types beyond streaming video to Netflix. A natural question is how these new entertainment types affect members’ satisfaction and their likelihood of continuing to subscribe to Netflix.</p><p>To analyze the impact of one of these new entertainment types, which we will call Type X, we wrote a simple analysis plan specifying our</p><ul><li><strong>Treatment</strong>: Days engaging with Type X (or “Type X days” for short)</li><li><strong>Outcome</strong>: Two-month retention</li><li><strong>Potential</strong> <strong>confounders</strong>, including pre-treatment Type X days</li></ul><p>To establish a baseline, we fed this analysis plan without additional scaffolding to Claude Sonnet 4.6, a powerful yet accessible general-purpose model. The model chose and executed a defensible analysis strategy: linearly regressing retention on Type X days along with controls.</p><p>While the result was polished and impressive, when we ran the same analysis through our paved path tooling and agentic workflow, also using Sonnet 4.6, our agent produced an updated estimate that was just 25% of the baseline! What explains the difference between the baseline and the paved-path estimates?</p><p>A core challenge when analyzing new entertainment types is <strong>early adopter bias</strong>. The first users of any new offering are likely to be systematically different from the general population. For example, they may be heavier users of Netflix generally, or they may be extremely strong fans of the underlying titles. Early adopter bias manifested in our analysis as poor “overlap”: the vast majority of observations had a small estimated probability of engaging with Type X, reflecting its early maturity.</p><p>This imbalance was caught by our critic agent in its writeup of the analysis. The critic also flagged the failure of the placebo test: early Type X adopters differed significantly from non-adopters in terms of important confounders measured <em>before</em> experiencing the treatment, a warning sign of potential bias.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ee0gswtciDVvuFr1QenlvQ.png" /></figure><h3>Addressing Failed Diagnostics</h3><p>To address these diagnostic failures, our workflow provides agents with a playbook. For example, to overcome poor overlap, we instruct the agent to use <a href="https://academic.oup.com/biomet/article-abstract/96/1/187/235329?redirectedFrom=fulltext">Crump-style trimming</a>. That is, before estimating causal effects, the actor trims units with estimated propensity scores outside the range [0.1, 0.9]. This scopes the treatment effect being estimated to the ATE in the population that is not very likely or unlikely to engage in the new entertainment type — an important caveat we instruct the critic to flag in its report.</p><p>Trimming yields an estimate that is much smaller than the baseline estimate, and which only applies to the “overlapping” population (for whom engagement with the new entertainment type is non-deterministic). However, the trimmed estimate is substantially more credible, as it focuses on the members for whom the treatment could plausibly be randomly assigned, as in a target trial.</p><p>Contrastively, the baseline effect relies heavily on assumptions to extrapolate outcomes for all members, even those with a very low probability of treatment. The <a href="https://gking.harvard.edu/files/counterft.pdf?utm_source=chatgpt.com">danger</a> here is that extrapolation produces a number that is not backed by robust data and is likely confounded by early adopter bias.</p><h3>Orchestrating Followup Analyses</h3><p>There are two natural followups to this analysis:</p><ol><li>First, we need to analyze the sensitivity of estimates to the choice of trimming threshold. Practically, this requires redoing the analysis with multiple trimming thresholds.</li><li>Second, we also care about how these causal effects evolve over time. Yet, comparing causal effects across time raises subtle challenges. For example, we need to coordinate the population across all analyses: if a set of users is trimmed to make one analysis more credible, it should be trimmed in the other analyses as well.</li></ol><p>Both of these followups require conducting multiple versions of the same analysis, tweaking some parameters while keeping others the same. Managing this complexity and ensuring consistent execution is another area where agents add value.</p><p>To illustrate this, below we show a sensitivity analysis for our case study in which we asked the agent to vary the trimming bounds from [0, 1] (no trimming) to [0.15, 0.85]. As the plot shows, the estimated ATE on the overlapping population is robust to the choice of trimming threshold within bounds of [0.005, 0.995]. Although principals could (and should) execute <a href="https://psicostat.github.io/4ms-winter-school/papers/Steegen%20et%20al.%202016%20-%20Increasing%20Transparency%20Through%20a%20Multiverse%20Analysis.pdf">this and other robustness analyses</a>, delegating them to agents helps to reduce toil.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GbQUhS5LB9gcn16VDnC69w.png" /></figure><p>Another example is generating a time series by repeating the same analysis across multiple date partitions. For example, below we plot the results of using our agent to refit a different analysis on ten distinct date partitions. The plot shows evidence of seasonality: the treatment has a stronger effect on the winter dates compared to the summer dates.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*792mstFbTU-1ZVSs7KJEVw.png" /></figure><h3>Public Repo and Evals</h3><p>To help OCI practitioners build on and contribute to our workflow, we are open-sourcing a standalone version of <a href="https://github.com/Netflix-Skunkworks/oci-agent">oci-agent</a>. This repo implements two evaluations on public datasets from the 2016 Atlantic Causal Inference Competition (ACIC) data analysis competition. It also includes a lightweight version of our internal causal machine learning notebook that only uses open-source software (<a href="https://github.com/py-why/EconML">EconML</a>).</p><p>Our first evaluation runs this notebook for three randomly sampled datasets generated by each of the 77 data-generating processes (DGPs) in the ACIC data. Next, it uses the critic to grade the resulting 231 estimates as either satisfactory or unsatisfactory based on the diagnostics.</p><p>Below, we plot the average RMSE and coverage of 95% confidence intervals of our ATT estimates against the 44 competitor methods in the ACIC competition. As the scatterplot shows, our statistical methodology is competitive against these benchmarks: it achieves reasonably low RMSE and well-calibrated confidence intervals that cover the truth in ~95% of DGPs.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nvNVibkHRO8EgqNcliB_Zg.png" /></figure><p>More to the point, our diagnostics and agentic workflow help to separate more reliable estimates from less reliable estimates. To illustrate this, the following chart plots our ATE estimates in terms of RMSE and coverage. Note that we separate out the RMSE and coverage of:</p><ul><li>All 231 estimates (purple dot)</li><li>The 192 satisfactory estimates (blue star)</li><li>The 39 unsatisfactory estimates (red dot)</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SGM_i82SxCI64iMIp-diFw.png" /></figure><p>As the plot shows, when aided by our diagnostic suite, the critic agent is able to separate good estimates from bad estimates: the satisfactory estimates have <em>much </em>lower RMSE and better calibrated confidence intervals than do the unsatisfactory estimates.</p><p>Our second evaluation compares the performance of an LLM on the same analysis plan with our scaffolding and without it (i.e., one-shot prompting). Unsurprisingly, we find that our scaffolding is critical to helping the LLM return useful estimates. This can be seen in the following random sample of ten ACIC datasets. Using our scaffolding, the LLM recovers the ground truth in nine out of ten datasets. Furthermore, estimates are highly correlated with ground truth.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*k105qpwAZ3bjY_kLvAEpUA.png" /></figure><p>In contrast, giving the same analysis plan to Sonnet 4.6 without any scaffolding (i.e., just prompting it) results in consistently wrong answers that are not at all correlated with ground truth.</p><p>A key limitation of our public repo is that, due to the synthetic nature of the underlying datasets, it doesn’t pressure-test our agent’s semantic understanding or performance on real-world OCI tasks. Nonetheless, the repo demonstrates the core principles underlying our workflow. These include (1) giving agents with extensive scaffolding so that they follow best practices by design, and (2) requiring inspectable artifacts so that humans can audit agents’ <em>processes,</em> not just their outcomes.</p><h3>Conclusion</h3><p>We provide a workflow for doing observational causal inference with the help of software agents. Leveraging elements of our pre-AI OCI toolkit, such as templated notebooks, our workflow is designed to ensure that agents conduct rigorous and exhaustive analyses. This helps to reduce the human toil of OCI, which can be a highly iterative and exacting process.</p><p>At the same time, motivated by the complexity and ambiguity of observational causal inference, our workflow seeks to be <strong>human-augmenting</strong> and enables human practitioners to evaluate each analytic step.</p><p>Using agents for causal inference poses a challenge: how do we evaluate agents’ performance on tasks without ground truth? To meet this challenge, our workflow combines process audits with human oversight. To enable others to learn from and critique our workflow, we have <a href="https://github.com/Netflix-Skunkworks/oci-agent">open-sourced</a> a lightweight, standalone version. We hope this work stimulates more research and development on agentic evaluation in the absence of ground truth.</p><p><em>For valuable feedback on this post and “dogfooding,” we thank Adith Swaminathan, Ayal Chen-Zion, Colin Gray, Juliet Hougland, and Simon Ejdemyr.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4623f0a9c5af" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Thinking Fast & Slow for a Personalized Notification System]]></title>
            <link>https://netflixtechblog.medium.com/thinking-fast-slow-for-a-personalized-notification-system-4d89b26525cd?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/4d89b26525cd</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[notifications]]></category>
            <category><![CDATA[recommendations]]></category>
            <category><![CDATA[machine-learning]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Fri, 05 Jun 2026 16:31:01 GMT</pubDate>
            <atom:updated>2026-06-05T16:31:01.113Z</atom:updated>
            <content:encoded><![CDATA[<p>by <a href="https://www.linkedin.com/in/matthew-wood-bbb32584/">Matthew Wood</a>, <a href="https://www.linkedin.com/in/ishangupta1993/">Ishan Gupta</a>, Kevin Mercurio, <a href="https://www.linkedin.com/in/devon-bryant-9910ab5/">Devon Bryant</a>, and <a href="https://www.linkedin.com/in/clairedorman/">Claire Dorman</a></p><p>In his seminal book “Thinking, Fast and Slow,” Daniel Kahneman describes two systems that drive human cognition: System 1, which operates automatically and quickly with little effort, and System 2, which allocates attention to more challenging mental activities requiring deliberate focus. This dual-process theory has profound implications not just for understanding human behavior, but for designing intelligent systems that must balance immediate responsiveness with strategic foresight. Similar “plan vs. act” decompositions show up in other domains too — for example, robotics and autonomous driving often separate a slower planning layer (setting goals and constraints over longer horizons) from faster control and execution loops, and modern LLM agents frequently pair deliberate planning with rapid, step-by-step tool use and reaction.</p><p>At Netflix, our messaging platform faces a similar challenge every day. We send hundreds of millions of personalized notifications — push messages, emails, and in-app alerts — to help members discover content they’ll love. This creates a central tension: optimizing each notification for near-term engagement can conflict with what is best for the member over the long term. Higher message frequency can increase fatigue and opt-out risk, while lower frequency can reduce awareness of relevant titles and features the member would value.</p><p>This blog post introduces our framework for personalized notifications — a hierarchical system where a “slow” policy makes strategic, personalized decisions about a member’s weekly messaging plan (e.g., the intended frequency per channel and the resulting pacing over the week), while a “fast” policy handles the tactical, real-time decisions about which specific message to send when a send opportunity occurs. Together, they balance near-term engagement with longer-term member experience.</p><h3>The Problem:</h3><p>Before introducing our new framework, it is helpful to ground the discussion in a representative baseline for a personalized notification system. In our previous production system, we used a causal model to make send decisions by predicting the causal effect of a single message over a short time horizon. While this approach is effective as a baseline, it suffers from two fundamental limitations:</p><h3>Short-Term Reward Horizons</h3><p>The single-message outcome model is trained to optimize short-horizon metrics, such as immediate user actions occurring shortly after a notification is sent. While this is excellent for driving near-term engagement, it misses the cumulative, long-term effects of a messaging strategy. A message that drives an interaction today might also contribute to notification fatigue, reducing responsiveness in the weeks to follow. Because critical indicators of member satisfaction — like sustained viewing habits or gradual opt-out risk — only surface over extended timeframes, a short-term model will always miss the bigger picture.</p><h3>Coupled Ranking and Pacing Decisions</h3><p>When a single system evaluates daily incrementality to decide both whether to send something and, if so, which item to send, an individual member’s weekly message frequency becomes a by-product of those daily decisions rather than an explicit control variable. In our previous single-policy system, frequency was controlled implicitly through a relevance threshold on the model score calibrated to achieve a target aggregate send rate. While effective for managing overall frequency, this mechanism limited the system’s ability to personalize frequency based on individual engagement patterns. Moreover, because send eligibility and message selection were coupled in the same decision rule, adjusting the threshold to control frequency also changed the distribution and quality of selected messages, and vice versa.</p><p>To solve these challenges, we needed a system that could separate longer-term strategy from shorter-term decisions. What if we could determine an optimal, personalized message plan for each member, and then focus on selecting the most relevant content within those bounds? In the following sections, we detail how we realized this vision by decoupling our notification engine into a hierarchical ‘System 1’ and ‘System 2’ framework.</p><h3>The Proposed Method: A Hierarchical Slow-Fast Architecture</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BffuQUAFrEhYzynBKsVPLA.png" /></figure><p>The Slow policy’s primary role is to define a <strong>personalized pacing of messages over a defined time horizon</strong>. The decisions made by slow policy are consumed by the Fast Policy whose role is to maximize immediate relevance and select the optimal message for the member at any given moment.</p><p>To illustrate the Slow Policy in practice: For example, if optimized at a weekly cadence, the policy evaluates a member’s long-term engagement patterns to select a “Pacing Plan Action.” To keep the action space manageable yet expressive, we discretize the decision space into a set of actions that independently specify push and email frequencies. This provides approximately O(100) distinct combinations of cross-channel pacing strategies.</p><p><strong>The Utility Function</strong></p><p>The Slow policy selects actions by maximizing a personalized utility function. This function explicitly trades off positive engagement signals against the long-term “cost” of messaging.</p><p><em>U(member, action) = Σ wₖ·Reward_k(member,action) — Cost(action)</em></p><p>To capture a holistic view of member health, this utility is composed of:</p><ul><li><strong>Positive Signals:</strong> Capturing the likelihood that a member will find value in and engage with the platform.</li><li><strong>Negative Signals:</strong> Capturing the likelihood of member fatigue or a propensity to opt out of a messaging channel.</li></ul><p>Ideally, negative signals alone would naturally penalize over-messaging. In practice, however, explicit negative feedback is extremely sparse. Without an additional constraint, the predicted ‘cost’ of an incremental message appears negligible, causing the model to gravitate toward maximum frequency.</p><p>To address this, we introduce a <strong>universal message cost</strong> that is added to the personalized negative‑feedback prediction for every send. This additional cost term keeps the reward function concave and well‑behaved, preventing degenerate “always send” policies. The message cost parameter is empirically tuned using a combination of online experiments and offline evaluation metrics.</p><p><strong>Pacing Strategy</strong></p><p>The two-stage design naturally allows for optimizing both the average frequency as well as pacing of messages over time. The simplest pacing strategy is uniform random: we translate the frequency target into a per-opportunity send probability and, at each eligible opportunity, effectively flip a weighted coin to decide whether to send. This produces an organically randomized pattern whose expected send rate matches the target.</p><p>While uniform pacing provides a clean and robust baseline, the framework readily extends to richer, non-uniform pacing profiles (for example, day-of-week patterns, conditioning on user activity, or launch-aligned bursts) whenever product or user-experience considerations call for more structured temporal distributions.</p><p><strong>Policy-to-Policy Communication</strong></p><p>The true power of this hierarchy lies in decoupling. By splitting into “Slow” and “Fast” policies, we allow each to focus on what it does best.</p><p>To bridge these two worlds asynchronously, decisions are events and state is managed through a low-latency feature store:</p><ul><li><strong>The Planner (Slow):</strong> The Slow policy calculates a member’s ideal pacing plan. It writes this strategic intent to a feature store</li><li><strong>The Executor (Fast):</strong> Every day, when a notification opportunity arises, the Fast Policy simply pulls that stored “plan” as a feature. It then executes the tactical send decision within those strategic guardrails.</li></ul><p>This architecture provides two critical advantages:</p><ol><li><strong>“Stickiness”:</strong> It ensures a member receives a consistent experience. The Slow policy will be executed once at a defined cadence; the plan is stored and honored.</li><li><strong>Independent Evolution:</strong> We can retrain, optimize, or A/B test our weekly pacing strategies (the “Slow” layer) without ever touching the real-time ranking logic (the “Fast” layer).</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gZiJ6HiK_BG3rDCRupx5vA.png" /><figcaption>Figure 1: Schematic of the two-layer message personalization system composed of a slow planning policy (top) and a fast execution policy (bottom). A feature store serves as the communication bridge between the two policies.</figcaption></figure><h3>Key Results &amp; Takeaways</h3><p>The transition to a hierarchical architecture resulted in one of our <strong>largest production metric lifts to date</strong>. We observed several key breakthroughs:</p><ul><li><strong>Empowering the “Casual Viewer”</strong>: Gains were most significant among members who watch less frequently — a critical cohort where timely, high-relevance awareness of new content is vital.</li><li><strong>The Power of Decoupling</strong>: Separating <em>frequency planning</em> from <em>message selection</em> was as transformative as the modeling itself. This new architecture unlocks incredible flexibility, allowing us to iterate on content ranking models and pacing strategies as two independent, clean variables.</li><li><strong>Respecting the Horizon</strong>: The impact of messaging is rarely an isolated event; its effects build up cumulatively based on ongoing interactions between our system and the member. By isolating pacing into a dedicated strategic layer, we now have the mechanism to explicitly manage long-term fatigue and opt-out risk.</li></ul><h3>Acknowledgments</h3><p>We could not have delivered this project without the help of our outstanding colleagues, and we sincerely thank them for their contributions.</p><p><strong>Feature Store Team</strong>: <a href="https://www.linkedin.com/in/aaronlewism/">Aaron Lewis</a>, <a href="https://www.linkedin.com/in/tom-switzer-59824356/">Tom Switzer</a>, <a href="https://www.linkedin.com/in/abbywh/">Abby Whittier</a>, <a href="https://www.linkedin.com/in/ray-zhang-a7168a32/">Ray Zhang</a><br><strong>Product: </strong><a href="https://www.linkedin.com/in/fenglinli/">Fiona Li</a><br><strong>AI for Member Systems (supporting contributor): </strong><a href="https://www.linkedin.com/in/sergipv/">Sergi Perez</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4d89b26525cd" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Dynamic Repartitioning for Time Series Workloads]]></title>
            <link>https://medium.com/netflix-techblog/dynamically-splitting-wide-partitions-in-cassandra-for-time-series-workloads-0eded064f456?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/0eded064f456</guid>
            <category><![CDATA[scalability]]></category>
            <category><![CDATA[cassandra]]></category>
            <category><![CDATA[database]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <category><![CDATA[timeseries]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Wed, 03 Jun 2026 01:40:22 GMT</pubDate>
            <atom:updated>2026-06-03T19:20:39.203Z</atom:updated>
            <content:encoded><![CDATA[<p>By <a href="https://www.linkedin.com/in/rajiv-shringi/">Rajiv Shringi</a>, <a href="https://www.linkedin.com/in/kaidanfullerton/">Kaidan Fullerton</a>, <a href="https://www.linkedin.com/in/oleksii-tkachuk-98b47375/">Oleksii Tkachuk</a> and <a href="https://www.linkedin.com/in/kartik894/">Kartik Sathyanarayanan</a></p><h3>Introduction</h3><p><a href="https://netflixtechblog.com/introducing-netflix-timeseries-data-abstraction-layer-31552f6326f8">Netflix’s TimeSeries Abstraction</a> is a scalable system for ingesting and querying petabytes of temporal event data with millisecond latency. We use <a href="https://cassandra.apache.org/_/index.html">Apache Cassandra</a> 4.x as the underlying storage for these main reasons:</p><ul><li><strong>Throughput, latency, and cost</strong>: Cassandra can handle millions of low‑latency reads and writes in a cost-effective manner.</li><li><strong>Operational maturity</strong>: Our data platform team has deep operational expertise running large Cassandra clusters in production.</li></ul><p>However, using Cassandra at this scale introduces trade‑offs for TimeSeries workloads. A key challenge is <a href="https://docs.datastax.com/en/cql/hcd/data-modeling/best-practices.html#bucketing">wide partitions</a>, as TimeSeries dataset partitions can grow quite large with events accumulating over time.</p><p>This problem is further compounded by the fact that TimeSeries servers routinely deal with a very high read throughput:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0l0prhKlcOjf-d3-JVQTJg.png" /><figcaption>Reads/second for different datasets</figcaption></figure><p>This post walks through our journey to reduce the impact of wide partitions in our TimeSeries datasets, the solutions we built, and the lessons we learned.</p><blockquote>Note: Although this post walks through re-partitioning in Cassandra, the same techniques can be applied more broadly to other data stores.</blockquote><h3>Impact of Wide Partitions</h3><p>For most of our datasets, we observe an average read latency in the order of single-digit milliseconds:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sAc4HAMIvLcKTlt9NqOmjQ.png" /><figcaption>Ideal Latency for Reads (ms)</figcaption></figure><p>However, in some datasets, as partitions grow too wide, we observe high read latencies in the order of seconds, especially towards the tail end:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uvqa9oj0P6uT-G27WPRJoA.png" /><figcaption>High Tail Latency for Reads (seconds)</figcaption></figure><p>This can result in timeouts:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gcmGxn6NdjRuy0HYQIDv9g.png" /><figcaption>Read timeouts / second</figcaption></figure><p>In extreme cases, if most of the reads target wide partitions, we can see Garbage Collection pauses, high CPU utilization and thread queueing.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lj0yVnacUHcE1RQ7iHRKVA.png" /><figcaption>High CPU utilization and thread-queueing in Cassandra clusters</figcaption></figure><p>Scaling up the underlying Cassandra cluster is always an option, but we need smarter alternatives than just throwing more money at the problem.</p><h3>TimeSeries Partitioning Strategy</h3><p>The TimeSeries Abstraction was designed to solve the problem of wide partitions by dividing the data into discrete time chunks. For more in-depth information, refer to our previous <a href="https://netflixtechblog.com/introducing-netflix-timeseries-data-abstraction-layer-31552f6326f8">blog</a>.</p><p>To summarize, here is an illustration of how TimeSeries partitioning strategy helps us break up wide partitions into manageable chunks.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ySI9hOE4_lSScqQ16KQAuw.png" /><figcaption>Time Series partitioning breaking up a dataset into Time slices, time buckets and event buckets</figcaption></figure><p>This strategy further allows us to efficiently query and drop data based on time, without having to deal with <a href="https://opencredo.com/blogs/cassandra-tombstones-common-issues">tombstones</a>.</p><h3>Picking the Partitioning Strategy</h3><p>When a namespace (a.k.a. dataset) is created, users must specify their anticipated workload characteristics. This specification is then fed into our <a href="https://github.com/Netflix-Skunkworks/service-capacity-modeling/blob/main/service_capacity_modeling/models/org/netflix/time_series.py">provisioning</a> pipeline. The pipeline processes these inputs, runs <a href="https://en.wikipedia.org/wiki/Monte_Carlo_method">Monte Carlo</a> simulations, and produces an optimal infrastructure and partition configuration.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*016OPfeTjQnGXnUSGdTIKA.png" /><figcaption>Provisioning picks optimal infra and configuration based on user inputs</figcaption></figure><p>You can learn more about our methodology of capacity planning in this insightful <a href="https://www.youtube.com/watch?v=Lf6B1PxIvAs">AWS re:Invent</a> talk given by one of our stunning colleagues.</p><h3>The Problem with the Current Approach</h3><p>Although this method of provisioning is effective in many situations, it proves insufficient for TimeSeries workloads under these conditions:</p><ul><li><strong>Workload is unknown or inaccurately estimated:</strong> Early on in a project, users can lack a reliable picture of production traffic or simply misestimate key parameters.</li><li><strong>Workload evolves over time:</strong> Traffic patterns, client behavior, and product requirements change. A “good” partitioning strategy on day one can become inefficient months later.</li><li><strong>Data outliers exist:</strong> Not all TimeSeries IDs behave the same. A small percentage of IDs can receive a vastly higher volume of events than the rest.</li></ul><p>Fortunately, our design with discrete Time Slices gives us a natural escape hatch for the first two scenarios; each new Time Slice can use a different partitioning strategy.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*F2gUdVv8NGAqoYbmFVMbnQ.png" /><figcaption>Each Time Slice can have a unique partition strategy</figcaption></figure><p>However, manually adjusting these configurations in a fleet that has thousands of TimeSeries datasets is not sustainable. We need automation.</p><h3>Solution 1: Time Slice Re-Partitioning</h3><p>Cassandra exposes useful introspection APIs for understanding data usage and access patterns. For example, <a href="https://docs.datastax.com/en/dse/6.9/managing/tools/nodetool/table-histograms.html">nodetool tablehistograms</a> provide percentile distributions for partition sizes in a table. Using these tools, we can detect cases of both over and under partitioning.</p><p>Below is an example of over‑partitioning, where the TimeSeries provisioning pipeline selected very small <em>time_bucket</em> intervals based on user provided inputs:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/811/1*yCT6SVX5682twAp2VhLPJg.png" /><figcaption>Provisioning selected 60s time buckets based on user inputs</figcaption></figure><p>causing partitions to have less than 10 KB of data, leading to high read amplification and thread queueing:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/920/1*XM-yySVKVu1YTyt9uPOk-w.png" /><figcaption>Histogram of the given Cassandra table showing partition size percentiles</figcaption></figure><p>In order to tune partition strategies efficiently, we added a background worker, which monitors partition histograms of Time Slices attached to a given application, and exposes it via a Cassandra virtual table:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZMZ1RYb52fha32uStM7w9A.png" /><figcaption>Histograms exposed through a Cassandra Virtual table</figcaption></figure><p>It then computes an adjustment factor when it detects partition sizes not meeting a configured density. This configured density is often set between 2 MiB to 10 MiB depending on the workload.</p><pre>DynamicTimeSliceConfigWorker: <br>namespace: my_dataset_1<br>Observed: TimeSlices have p99 partitions below configured target of 10MB. <br>Proposed: time_bucket interval: 60s -&gt; 604800s</pre><p>The worker can then update <em>future</em> Time Slices with the new partition strategy:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/710/1*HUAyyJeONB-5HVnMihAx8g.png" /><figcaption>Partitioning adjusted for future Time Slice(s)</figcaption></figure><p>This strategy has yielded real results in reducing our read latencies, as well as reducing the number of timeouts caused by thread queueing.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tE44f8HSDXCwvlffOq0w4A.png" /><figcaption>Reduction in tail latency and thread queueing for</figcaption></figure><p>However, this strategy only works if most of the data exhibits such behavior that warrants re-partitioning of the entire table. It does not work in cases where only a percentage of IDs within the table are wide.</p><p>We have a couple of options here:</p><ul><li><strong>Do Nothing</strong>: This is sometimes the right approach if there is no observed impact to the application’s top-level metrics.</li><li><strong>Partial Returns</strong>: We implemented a ‘Partial Return’ feature, which aborts an inflight request if it has breached a configured latency SLO, while returning whatever data it has collected up until that point. This is a great option for clients who care more about latency than fetching <em>all</em> the data.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*t7cz4h3Ebxg-HDyaMVIp_g.png" /><figcaption>Tail latency drops around the SLO cutoff as Partial Returns are enabled</figcaption></figure><ul><li><strong>Block IDs:</strong> This is an extreme step but worth mentioning, because we do deal with bad data that occasionally seeps into the system e.g. test or spam IDs that can make the system unstable.</li></ul><pre>dgwts.config.&lt;dataset&gt;.block.Ids: &quot;&lt;tsid-1&gt;, &lt;tsid-2&gt;, &lt;tsid-3&gt;&quot;</pre><p>Ultimately, we encounter scenarios where valid and important TimeSeries IDs accumulate a high enough volume of events, with callers needing to process all the related data. Simply tolerating elevated latencies or timeouts when querying these IDs is not a desirable outcome.</p><p>This is where dynamic partitioning comes into play.</p><h3>Solution 2: Dynamic Partitioning per ID</h3><p>Dynamic partitioning is an asynchronous pipeline that auto-detects and splits wide partitions on a TimeSeries ID level rather than at the table level.</p><p>It has three main stages:</p><ul><li><strong>Detection</strong>: Detects wide partitions for a given TimeSeries ID during the read path.</li><li><strong>Planning &amp; Splitting</strong>: Plans and executes splits of those partitions into optimal sizes asynchronously.</li><li><strong>Serving Reads</strong>: Re-routes the read queries transparently to read data from the split partitions when ready.</li></ul><p>This is how it works at a high level; we will dive into details after:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tucaTmgbj6BqhA8YKmUn9A.png" /><figcaption>Dynamic Wide Partition Split Async Pipeline</figcaption></figure><p>Here are the different stages of the pipeline:</p><h4>Detection</h4><p>Every TimeSeries read operation tracks how many bytes are read for a given partition. If the bytes read exceed a configured threshold, the server emits a detection event to Kafka:</p><pre>{<br>  &quot;time_slice&quot;: &quot;data_20260328&quot;, // the Cassandra table this event was detected in<br>  &quot;time_series_id&quot;: &quot;profileId:123&quot;, // the ID detected as wide<br>  &quot;time_bucket&quot;: 7, // the existing time_bucket partition<br>  &quot;event_bucket&quot;: 2, // the existing event_bucket partition<br>  &quot;immutable&quot;: true, // TimeSeries servers can compute if this partition is no longer receiving writes<br>  &quot;version&quot;: &quot;0&quot; // reserved for future use e.g. invalidate if partition is no longer immutable<br>}</pre><p>Our decision to detect wide partitions on reads, as opposed to writes, is based on our observation that the majority of the data in the wild doesn’t need this treatment. The slight downside is that some reads on these large partitions may suffer sub-optimal performance for a very short duration (typically seconds) until this process catches up.</p><h4>Immutability</h4><p>Although splitting mutable partitions is possible, it is inherently more complex. As a first step towards solving this problem, we chose to reduce the surface area of this change by focusing on immutable partitions, while still meaningfully reducing caller timeouts.</p><h4>Planning</h4><p>Detection may occur based on a partial read, so the planner must still read the entire partition <em>once</em> to compute an accurate split plan. The checkpointing becomes crucial here. For planning reads that fail to process the entire partition, the process can always continue from the last saved checkpoint.</p><h4>Checkpointing</h4><p>The <em>wide_row</em> metadata table serves as the backbone for state transitions and checkpointing of partition splits. It also stores information that is used later by TimeSeries servers to properly route Read queries.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MNIBbz9hTPz-7ah8FRqTZQ.png" /><figcaption>wide_row metadata for storing split states and checkpoints</figcaption></figure><h4>Splitting</h4><p>The Planner delegates the splitting of data to an appropriate split-strategy. For example, if <em>EventBucketPartitionSplitStrategy</em> is selected, we split the partition by assigning more event buckets to the same time bucket. If the partition is <em>ultra-wide</em>, we cap the number of event buckets we split into, in order to control the resultant read amplification. Spreading into multiple partitions in such cases is still beneficial in order to spread the read workload to multiple Cassandra replicas.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Acbtr-hKcuopZ_7tXpWoxQ.png" /><figcaption>Split by assigning more event buckets for a given time bucket</figcaption></figure><p>Further, since the Splitter has the full view of the partition, it can ensure total sort order across all the split buckets.</p><h4>Validating Splits</h4><p>The Planner stores a pre-split checksum of a given partition during the planning phase, while the Splitter computes and stores the post-split checksum. The split status is marked as completed only if the two checksums match.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/997/1*BbADzQfFTZPWUbpORlXfGQ.png" /><figcaption>Ensure checksums match pre- and post-split before marking a split as COMPLETED</figcaption></figure><h4>Tracking Splits</h4><p>The pre- and post-split partition sizes across different datasets are tracked to see how effectively the partition splits are being planned and executed:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tif_TQsk2OkBV9CCWrfDJQ.png" /><figcaption>Track pre- and post-split partition sizes to ensure we are splitting optimally</figcaption></figure><h4>Serving Reads</h4><p>The TimeSeries servers load the partition-keys of completed splits periodically into in-memory Bloom filters. Every read operation checks the Bloom filter to see whether a query can be diverted to the split partitions.</p><p>Here is what the Read path looks like:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1WeYb1fTJq8-nkkMr-RjzQ.png" /><figcaption>Read path for diverting reads to existing or split partitions</figcaption></figure><p>The size of the Bloom filters is monitored to ensure we have enough memory per server. Due to the compactness of partition keys, and ratio of wide partitions in a given dataset, the filters fit comfortably in each server instance.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*73ahHVoNRUYaczuEn9KTBA.png" /><figcaption>Bloom filter approximate element count per namespace and time slice</figcaption></figure><p>The Bloom filter latency to check whether a given partition key is wide for every read request is typically in single-digit microseconds or better, making this diversion practically invisible to the callers.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*t1ZASboghIXdGDyHrz0Z0Q.png" /><figcaption>Latency for checking Bloom filters is extremely small for callers to notice the diversion</figcaption></figure><p>For the cases that do end up with a Bloom filter hit, the TimeSeries servers lookup the <em>wide_row</em> metadata to see how to read a specific wide partition:</p><pre>{<br>  &quot;pre_split_data&quot;: {<br>    &quot;time_slice&quot;: &quot;data_20260328&quot;,<br>    &quot;time_series_id&quot;: &quot;6313825&quot;, → What to read<br>    &quot;time_bucket&quot;: 0,<br>    &quot;event_bucket&quot;: 2<br>    …<br>  },<br>  &quot;post_split_data&quot;: {<br>    &quot;time_slice&quot;: &quot;wide_data_20260328_0&quot;, → Where to read it from<br>    &quot;event_bucket_partition_strategy&quot;: { → Strategy to delegate to for reading<br>    &quot;target_event_buckets&quot;: 2,<br>    &quot;start_event_bucket&quot;: 32 → How should the strategy read it<br>  }<br>  …<br>}</pre><p>This metadata read is backed by a read-through cache, making it quite performant:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CprO4zk8UQFYulId8jHdUg.png" /><figcaption>Metadata fetch latency is quite low to affect read operations</figcaption></figure><p>Finally, the reads for the split partitions are delegated to our existing <em>PartitionReader</em>, which reads <em>N smaller partitions in parallel</em>, rather than 1 large partition, improving overall performance and stability!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CHPEUf2WlJzHyUGdNiEA3A.png" /><figcaption>Read much smaller partitions in parallel and merge results</figcaption></figure><h4>Fallbacks</h4><p>The existing wide partition from the original time slice is never deleted. This helps us in creating safe fallbacks in many different scenarios of partial failures and eventual consistency. The slightly larger storage space we use as a result is worth the operational safety we gain.</p><h4>Building Additional Confidence</h4><p>Serving incorrect reads would be disastrous. To establish trust beyond checksums, we leveraged additional mechanisms such as:</p><ul><li>Using our existing <a href="https://netflixtechblog.medium.com/data-bridge-how-netflix-simplifies-data-movement-36d10d91c313">Data Bridge</a> pipelines to verify splits offline:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AspDR7sF38JxFXTkaK1wWQ.png" /><figcaption>Spark job to ensure that the split data is an exact match to the original data</figcaption></figure><ul><li>Implementing a phased rollout strategy to safely advance through stages as our confidence in the system grew:</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*la6WvWA4KWUglIPWpG27qw.png" /><figcaption>Advance through Read modes once previous mode passes checks</figcaption></figure><p>A critical part of this phased rollout was the <strong>Comparison</strong> phase, which compared bytes served by old read path and the new read path while in <em>shadow</em> mode:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sELeFr6fngTGib_gcD2ejA.png" /><figcaption>A chart of bytes match vs bytes differ in a given shadow period</figcaption></figure><h4>Results</h4><p>As a result of these dynamic splits, we see a huge improvement in the average read latency of most wide partitions, bringing it down from seconds:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AZuNdkiqBRtlcLhJBC-MzA.png" /><figcaption>Existing average latency for reading wide partitions</figcaption></figure><p>to <em>low double-digit milliseconds!</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*y8H7tjqj-g2-ZRGPBkUl_A.png" /><figcaption>Average latency for reading dynamically split partitions</figcaption></figure><p>Tail latencies of reading wide partitions dropped from several seconds:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JbNIJYjARHjGJH77-FiV7w.png" /><figcaption>Existing tail latency for reading wide partitions</figcaption></figure><p>to around 200 ms or better:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qKneKuGrpiU0VoMRw7OOZQ.png" /><figcaption>Tail latency for reading dynamically split partitions</figcaption></figure><p>resulting in a drop in read timeouts:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UjFSHe5HeZeDBKbmayLdnw.png" /></figure><p>Overall, this has resulted in a more stable Cassandra cluster with lower CPU utilization and little to no thread queuing:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Wt_oTudRIz8XKmJ5N3FcDA.png" /><figcaption>Low CPU utilization and no thread-queueing</figcaption></figure><p>Further, for extreme wide rows, where a dataset would face constant timeouts and unavailability blips, the service was able to paginate and query 500MB+ partitions while remaining available:</p><pre>grpc … com.netflix.dgw.ts.TimeSeriesService/SearchEventRecords -d<br>&#39;{&quot;namespace&quot;: &quot;...&quot;,<br>    &quot;search_query&quot;: {...},<br>    &quot;time_interval&quot;: {<br>      &quot;start&quot;: &quot;2026–05–11T23:42:51.484398Z&quot;,<br>      &quot;end&quot;: &quot;2026–05–12T00:13:50.694205Z&quot;<br>    },<br>    &quot;pageSize&quot; : 1000,<br>  }&#39;<br># Response:<br>{<br>  &quot;next_page_token&quot; : ….,<br>  &quot;records&quot;: [<br>    {<br>      …<br>    }<br>  ],<br>  &quot;response_context&quot;: [{<br>    &quot;namespace&quot;: &quot;...&quot;,<br>    …<br>    # Trades elevated latency for being available<br>    &quot;time_taken&quot;: &quot;41.072410142s&quot;<br>    }<br>  ]<br>}</pre><h3>Conclusion</h3><p>There is more work planned around this feature, like splitting <em>mutable</em> wide partitions, or re-processing previously failed splits, but this has been a successful start in improving service performance and reducing our support burden.</p><p>Further, we would like to highlight some key lessons that we learned at different points in this journey.</p><ul><li><strong>Reducing Surface Area: </strong>As a first step, explore simpler solutions that can still deliver meaningful impact. Also, reducing the surface area of a complex change and deploying incrementally pays off operationally.</li><li><strong>Building Confidence</strong>: Invest time and resources to build confidence in new features, especially when justified by the feature complexity, deployment blast radius, and/or potential impact.</li></ul><p><strong>Acknowledgements</strong>: Special thanks to our stunning colleagues who further contributed to this feature’s success: <a href="https://www.linkedin.com/in/tomdevoe/">Tom DeVoe</a>, <a href="https://www.linkedin.com/in/clohfink/">Chris Lohfink</a>, <a href="https://www.linkedin.com/in/sumanth-pasupuleti/">Sumanth Pasupuleti</a> and <a href="https://www.linkedin.com/in/joseph-lynch-9976a431/">Joey Lynch</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0eded064f456" width="1" height="1" alt=""><hr><p><a href="https://medium.com/netflix-techblog/dynamically-splitting-wide-partitions-in-cassandra-for-time-series-workloads-0eded064f456">Dynamic Repartitioning for Time Series Workloads</a> was originally published in <a href="https://medium.com/netflix-techblog">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[From Silos to Service Topology: Why Netflix Built a Real-Time Service Map]]></title>
            <link>https://medium.com/netflix-techblog/from-silos-to-service-topology-why-netflix-built-a-real-time-service-map-0165ba13a7bc?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/0165ba13a7bc</guid>
            <category><![CDATA[distributed-systems]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[platform-engineering]]></category>
            <category><![CDATA[microservices]]></category>
            <category><![CDATA[observability]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Fri, 29 May 2026 14:01:01 GMT</pubDate>
            <atom:updated>2026-05-29T18:45:18.988Z</atom:updated>
            <content:encoded><![CDATA[<p><em>By </em><a href="https://www.linkedin.com/in/parth-jain-8a09abb6/"><em>Parth Jain</em></a>, <a href="https://www.linkedin.com/in/raskuma/"><em>Rakesh Sukumar</em></a><em>, </em><a href="https://www.linkedin.com/in/yingwu-zhao-62037418/"><em>Yingwu Zhao</em></a><em>, </em><a href="https://www.linkedin.com/in/renzosanchezsilva/"><em>Renzo Sanchez</em></a><em> &amp; </em><a href="https://www.linkedin.com/in/nathfisher/"><em>Nathan Fisher</em></a><em><br>How we built a living map of our distributed infrastructure to help engineers understand dependencies, troubleshoot faster, and keep Netflix running smoothly for our members around the world.</em></p><h3>The Puzzle with a Thousand Pieces</h3><p>Picture this: It’s 3am, and an engineer gets paged. One of our critical services is showing elevated error rates. Members trying to watch their favorite films and series are seeing degraded experiences. The clock is ticking.</p><figure><img alt="A central service node connected to multiple downstream services and data stores, illustrating the tangled dependency graph engineers must navigate without a service topology map." src="https://cdn-images-1.medium.com/max/799/1*t_oo96UujawlqwGtmBBm7Q.png" /><figcaption>A single service at the center of a web of dependencies — services, data stores, and call chains branching in every direction. Without a unified map, engineers have to reason about this structure from memory and scattered signals.</figcaption></figure><p>In a system with thousands of microservices supporting our entertainment experience for members worldwide, answering these questions quickly can mean the difference between a minor blip and a major incident.</p><p>We kept hearing variations of this story from engineers across Netflix. The tooling gap was clear: we had plenty of signals, but no unified way to understand how everything connected.</p><h3>The Three Questions Every Engineer Asks</h3><p>When troubleshooting distributed systems, engineers fundamentally need to understand relationships:</p><p><strong>Which services depend on each other?</strong> Not just theoretical dependencies from configuration files or architecture diagrams, but actual runtime connections based on real traffic.</p><p><strong>What’s the blast radius?</strong> When something breaks or needs to go down for maintenance, what else will be affected? Which teams need to be notified?</p><p><strong>Where’s the source?</strong> Is my problem caused by an upstream issue, or am I the root cause that’s cascading to others?</p><p>Traditional observability tools show fragments of this picture. Metrics show symptoms and performance characteristics. Logs show individual service behavior. Traces show single request flows through the system. But none of them show the complete map of how everything connects — the steady-state topology of dependencies that forms the backbone of our distributed architecture.</p><p>For an engineer at 3am, having to mentally stitch together information from multiple tools is slow, error-prone, and stressful. We needed something better: a unified view of service dependencies — a map showing how everything connects — with easy navigation to the detailed signals when you need to dig deeper.</p><h3>Why This Matters More Than Ever</h3><p>Netflix runs on thousands of microservices working together to deliver entertainment to our members. When you press play on your favorite series, that single action triggers a cascade of service-to-service calls — authentication, recommendations tailored to your tastes, video encoding selection, playback optimization, and more.</p><p>This architecture gives us tremendous flexibility and allows hundreds of engineering teams to innovate independently. But it also creates fundamental observability challenges.</p><p>And these challenges were growing. New initiatives like our Live programming and Ads-supported plans require even more sophisticated monitoring and faster troubleshooting. Live events can’t wait for lengthy incident investigations. The scale and real-time nature of these systems demanded better tooling.</p><p>We analyzed thousands of support requests from our engineers over a four-year period. The patterns were consistent:</p><ul><li>“What are my upstream and downstream dependencies?”</li><li>“Is this failure in my service, or is something I depend on broken?”</li><li>“Which services will be impacted if I take this down for maintenance?”</li><li>“Why is this service showing as ‘Unknown’ in my metrics?”</li><li>“What changed in my call path recently that could explain this behavior?”</li></ul><p>Engineers were asking dependency questions constantly. We needed to provide answers — quickly, accurately, and in real-time.</p><h3>Building on What We Learned</h3><p>We didn’t start from scratch. Over the years, we explored various approaches to solving this problem — from evaluating external graph databases and vendor platforms to building internal prototypes with different storage technologies and data models.</p><h4>Each iteration taught us something valuable:</h4><p><strong>Real-time matters:</strong> Dependency maps that are hours old are useless in dynamic environments where services deploy multiple times per day. We needed near real-time updates.</p><p><strong>Scale changes everything:</strong> Solutions that work at modest scale hit fundamental walls at Netflix scale. Storage systems that handle thousands of nodes struggle with our service count and traffic volume.</p><p><strong>Integration is key:</strong> Any solution needs seamless integration with our existing observability ecosystem. Engineers shouldn’t have to learn entirely new tools or leave their existing workflows.</p><p><strong>Data quality is critical:</strong> Incomplete or incorrect dependency information is worse than no information — it leads to wrong conclusions during incidents.</p><p><strong>Multiple perspectives needed: </strong>We learned that no single source of dependency information tells the complete story. Network connectivity data lacks application context. Application metrics only cover instrumented services. We needed to combine multiple sources.</p><p>These lessons shaped every decision we made in building Service Topology.</p><h3>What We Needed: A Living Map</h3><p>We set out to build something specific: a living map of our infrastructure — one that updates in real-time as services deploy, as traffic patterns shift, as new dependencies form and old ones disappear.</p><p>The requirements were clear:</p><p><strong>Real-time updates, not stale snapshots:</strong> In an environment where services deploy continuously, yesterday’s topology map is archaeology, not observability.</p><p><strong>Fast queries at scale:</strong> When an engineer is troubleshooting at 3am, they can’t wait minutes for a query to return. We needed sub-second response times for traversing the call graph.</p><p><strong>Multiple layers:</strong> Network-level connectivity doesn’t tell the whole story. We needed to see both the network layer (what’s actually talking to what) and the application layer (which APIs and endpoints are being called).</p><p><strong>Rich context, not just connections:</strong> Knowing Service A talks to Service B isn’t enough. We needed to overlay health status, availability tiers, business domains, ownership information, and other metadata to make the information actionable.</p><p><strong>Visual and programmatic access:</strong> Engineers needed a UI for exploration and troubleshooting. But automated systems — resilience frameworks, blast radius calculators, incident response automation — needed programmatic API access.</p><h3>Our Approach: Three Sources of Truth</h3><figure><img alt="Three topology layers side by side: eBPF flow logs producing a network graph, IPC metrics producing an application graph, and distributed traces producing a request graph, all feeding into a unified view." src="https://cdn-images-1.medium.com/max/996/1*68tW9-7kC3oGL0T5J5di_A.png" /><figcaption>Three data sources produce three independent topology graphs — network, application, and request — each stored separately and queryable on their own or merged into a single unified view.</figcaption></figure><p>Here’s the key insight we arrived at: no single source tells the complete story.</p><p>We built Service Topology by using three complementary sources to build separate dependency graphs — one from each perspective — that can be combined into a unified view or explored independently:</p><p>Each source creates its own graph that is physically separate — the network layer in one graph database partition, the IPC layer in another partition, and the tracing layer using columnar storage optimized for analytical queries. This physical separation allows each layer to evolve independently and be queried in parallel. When users request a unified view, we execute traversal queries across all layers simultaneously and merge results, achieving sub-second response times even when combining all three layers.</p><p>Each source creates its own graph of service relationships:</p><h4>1. eBPF Network Flows (Network Layer)</h4><p>We capture network flow records at the kernel level using eBPF technology — information about which services are connecting to which other services over the network. This gives us ground truth about actual network-level communication.</p><p>The value: Comprehensive coverage. Every service shows up here because we’re capturing actual network traffic, regardless of whether applications are instrumented. This layer provides topology at both cluster-level (which deployment clusters are communicating) and app-level (which applications are communicating).</p><p>The limitation: Network-level information lacks application context. We know Service A connected to Service B’s IP address using a specific protocol, but not which specific API endpoint or path was called (e.g., /api/v1/users vs /api/v1/orders).</p><h4>2. IPC Metrics (Application Layer)</h4><p>We collect Inter-Process Communication metrics from our instrumented services. These are the metrics applications emit when they make calls to other services via gRPC, GraphQL, REST, or other protocols.</p><p>The value: Rich application context. We can see which specific endpoints were called, error rates, latency distributions, protocol details, and request/response characteristics. This layer provides app-level topology — since IPC metrics are emitted by applications, the natural granularity is application-to-application connections with endpoint details.</p><p>The limitation: Only works for instrumented services. If a service doesn’t emit IPC metrics, we won’t see its application-level calls this way.</p><h4>3. End-to-End Tracing (Request Layer)</h4><p>We integrate distributed tracing information that follows individual requests as they flow through our system. We aggregate traces to build a unified topology graph, but also allow engineers to overlay individual traces on the topology to see specific request flows.</p><p>The value: Shows actual request paths. Not just “Service A <em>can</em> call Service B,” but “Service A <em>did</em> call Service B as part of serving this specific member request.” This captures runtime behavior, including conditional logic and feature flags. Engineers can both see the aggregated pattern and drill into individual traces. We aggregate traces to build topology at both cluster-level and app-level, allowing engineers to view request patterns at the granularity most useful for their investigation.</p><p>The limitation: Sampling. We can’t trace every request without impacting performance, so we sample. This is excellent for understanding common flows, but may miss rarely-used code paths in the aggregated view.</p><h4>Bringing It Together: Multi-Layer Architecture</h4><p>Here’s what makes this powerful: we build three separate graphs — one from each source — that create different perspectives on service relationships:</p><ul><li><strong>Network graph from eBPF flows:</strong> Every connection, regardless of instrumentation</li><li><strong>Application graph from IPC metrics:</strong> Rich endpoint and protocol details</li><li><strong>Request graph from tracing:</strong> Actual runtime behavior and call paths</li></ul><p>Engineers can:</p><ul><li>View each graph independently to focus on a specific perspective (pure network connectivity, application-level calls, or traced request flows)</li><li>Combine them into a unified graph by querying multiple partitions in parallel and merging results — our system returns the union of nodes and edges from all requested layers while preserving each layer’s distinct properties</li></ul><p>The unified view is especially powerful because:</p><ul><li>Network flows ensure completeness — we don’t miss anything</li><li>IPC metrics provide application details — we understand the “how” and “what”</li><li>Tracing shows actual behavior — we see real request patterns</li></ul><p>Each source compensates for the limitations of the others. The result is a comprehensive, accurate, and contextualized view of service dependencies that can be explored from multiple angles.</p><h3>From Flows to Graph: How We Built It</h3><p>Here’s the high-level architecture (we’ll dive deeper into engineering challenges in our next post):</p><figure><img alt="Pipeline diagram showing data flowing from a message stream through Stage 1 initial aggregation, Stage 2 intermediary resolution, and Stage 3 persistence and enrichment into a graph database, then exposed via an API." src="https://cdn-images-1.medium.com/max/1024/1*bvSG8r3B-fffrr-2ZKCtpA.png" /><figcaption>Flow logs travel from multi-region Kafka through three aggregation stages — initial batching, intermediary resolution, and final enrichment — before being persisted to the graph database and served via API.</figcaption></figure><p><strong>Multi-Region Ingestion:</strong> We consume flow logs from Kafka across multiple AWS regions where Netflix operates. This runs continuously, processing millions of flow records as they arrive.</p><p><strong>Distributed Processing:</strong> We use Apache Pekko Streams (a fork of Akka) to process these flows in a distributed, fault-tolerant pipeline. The system automatically partitions work across our Auto Scaling Groups to handle the volume and provides natural backpressure handling.</p><p><strong>Three-Stage Distributed Aggregation</strong>: We aggregate network flows through a three-stage pipeline that solves a fundamental challenge: network flow logs only show individual network hops through intermediaries (App A → Load Balancer → App B, or App A → NAT Gateway → App B), not the true application-level connections we need (App A → App B).</p><figure><img alt="Before and after diagram showing intermediary resolution: raw flow logs recording two hops from App A through a load balancer to App B are collapsed into a single direct edge from App A to App B." src="https://cdn-images-1.medium.com/max/292/1*UcZvGrHzMq6geyat9MZv7g.png" /><figcaption>Stage 2 resolves network intermediaries: raw flow logs show two separate hops (App A → Load Balancer → App B), but the resolved graph stores the direct application-to-application relationship (App A → App B).</figcaption></figure><p>Stage 1 performs initial aggregation from Kafka. Stage 2 applies resolution logic — identifying network intermediaries (load balancers, NAT gateways, API gateways, proxies) and combining their incoming and outgoing flows to reconstruct direct application-to-application paths. Stage 3 performs final aggregation with health status integration before graph persistence. This graduated approach also prevents hot spots by distributing load across multiple points even when specific applications or network intermediaries see 100x more traffic than others.</p><p>Graph Storage: We persist the topology in <a href="https://netflixtechblog.medium.com/high-throughput-graph-abstraction-at-netflix-part-i-e88063e6f6d5">Netflix’s graph database</a>, an abstraction layer built on top of our distributed key-value storage infrastructure. This graph database is specifically designed for high-throughput graph operations at our scale, with fast multi-hop traversal capabilities. Each of our three data sources (network flows, IPC metrics, tracing) creates a separate graph that can be queried independently or merged.</p><p>gRPC API: We expose the topology through a gRPC service that supports multi-hop traversal, filtering by availability tier and business domain, pagination for large result sets, and sub-second query response times.</p><p>The technical details of building this at Netflix scale — handling Kafka lag, managing memory and garbage collection, optimizing distributed processing, debugging reactive streams — deserve their own discussion. We learned a lot, and we’ll share those lessons in our next post.</p><h3>What Engineers Can Do Now</h3><p>Today, the service topology map is helping engineers across Netflix:</p><p><strong>Visualize Dependencies:</strong> See upstream and downstream dependencies for any service, with the ability to filter by availability tier (Tier 0, Tier 1, etc.) and business domain. Choose between the unified view (combining all sources) or individual graph views (network-only, IPC-only, or trace-only) depending on what you’re investigating.</p><p><strong>Jump to Detailed Signals: </strong>From any service in the topology, quickly navigate to logs, traces, and detailed metrics in their respective tools. No more hunting for the right service name or time window — the topology provides the context and the starting point.</p><p><strong>Understand Blast Radius:</strong> Before taking a service down for maintenance or making significant changes, see exactly what will be impacted. Identify which teams to notify and what to monitor.</p><p><strong>Overlay Health Status:</strong> See not just the topology, but which services in the call path are experiencing issues. This is integrated with health status tracking, so you can quickly identify if a problem you’re seeing is actually originating somewhere else.</p><p><strong>Query Programmatically:</strong> Use our gRPC API to integrate topology information into automated systems. For example, our Platform Modernization Engineering team uses this to verify that critical Live services have proper availability tier classifications throughout their dependency chains.</p><p><strong>Investigate Faster:</strong> During incidents, quickly identify if a failure is local or if it’s propagating from somewhere else in the call graph. Follow the failure pattern to find the root cause.</p><p><strong>Plan Changes Confidently:</strong> Understand the impact of proposed architectural changes or service migrations before implementing them.</p><p><strong>Time Travel Through Topology:</strong> Query what the topology looked like at specific points in the past. Understand what changed in dependencies around the time an issue started, or see how your service’s dependency footprint has evolved over time. This time-travel capability is powered by time-window aggregation — instead of storing every time slice separately, we use layer-specific aggregators that accumulate topology data across windows, allowing us to reconstruct historical views efficiently without exploding storage costs.</p><h3>The Living Map: Always Current</h3><p>What makes this truly useful is that it’s a living map. It’s not a static diagram drawn in a design document that goes out of date the moment it’s published. It’s continuously updated based on actual traffic:</p><ul><li>When a new service starts calling an API, it appears in the topology with near real-time freshness</li><li>When a service stops making calls to a dependency, that edge fades from the graph</li><li>When services deploy and their behavior changes, the topology reflects it</li><li>When incidents impact service health, the status overlay updates in real-time</li></ul><p>This means engineers can trust what they see. The map reflects reality, not someone’s idea of what the architecture should be.</p><h3>The Journey Continues</h3><p>We’re not done. We continue to evolve the system with new capabilities:</p><p>Change Event Overlay: We’re working to surface deployment events, configuration changes, and other mutations alongside the topology graph. Correlation becomes easier when you can see both the dependencies and what changed when.</p><p>Richer Context: As we expand coverage and integrate more signals, we continue to enrich the topology with additional endpoint-level details, protocol information, and network path context.</p><p>And looking further ahead, we’re excited about something bigger: Automated root cause analysis. Imagine an intelligent agent that continuously crawls the topology graph, correlates failures across dependencies, understands historical patterns, and surfaces likely root causes automatically. Service topology provides the knowledge graph foundation that makes this kind of intelligent automation possible.</p><h3>Why This Matters for Our Members</h3><p>This might seem like infrastructure — plumbing that our members never see directly. But it matters immensely to their experience.</p><p>When engineers can quickly understand dependencies and identify issues, incidents get resolved faster. When we can model blast radius before making changes, we avoid disruptions. When automated systems can query dependency information programmatically, we can build smarter, more resilient systems.</p><p>All of this translates to what matters most: our members getting to watch their favorite films and series, seamlessly, whenever they want. Whether it’s a weekend binge of a beloved show, a live sports event, or discovering something new through our recommendations tailored to their tastes — we want it to just work.</p><h3>What’s Next in This Series</h3><p>This is the first in a series of posts about building Service Topology at Netflix.</p><p>In our next post, we’ll pull back the curtain on the engineering challenges we faced at scale: How do you handle Kafka consumer lag when ingesting millions of flow logs per second? What happens when distributed processing meets garbage collection pauses? How do you debug reactive streams that stall under load? How do you manage hot nodes in a distributed system? We’ll share the real problems we hit in production and the solutions we developed.</p><p>In future posts, we’ll explore the lessons we learned that apply to any distributed system at scale, and where we’re heading next with time travel capabilities and Automated root cause analysis.</p><h3>Acknowledgements</h3><p><em>This post was written by </em><a href="https://www.linkedin.com/in/parth-jain-8a09abb6/"><em>Parth Jain</em></a><em>.</em></p><p><em>Service Topology was built by </em><a href="https://www.linkedin.com/in/parth-jain-8a09abb6/"><em>Parth Jain</em></a><em>, </em><a href="https://www.linkedin.com/in/raskuma/"><em>Rakesh Sukumar</em></a><em>, </em><a href="https://www.linkedin.com/in/yingwu-zhao-62037418/"><em>Yingwu Zhao</em></a><em>, </em><a href="https://www.linkedin.com/in/renzosanchezsilva/"><em>Renzo Sanchez-Silva</em></a><em>, and </em><a href="https://www.linkedin.com/in/nathfisher/"><em>Nathan Fisher</em></a><em>.</em></p><p><em>Special thanks to the many engineers across Netflix who made this possible — the Observability team who built the broader system, the graph database platform team who provided the storage foundation, and the Platform Modernization Engineering, Live, and Ads teams who provided invaluable feedback and use cases throughout development.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0165ba13a7bc" width="1" height="1" alt=""><hr><p><a href="https://medium.com/netflix-techblog/from-silos-to-service-topology-why-netflix-built-a-real-time-service-map-0165ba13a7bc">From Silos to Service Topology: Why Netflix Built a Real-Time Service Map</a> was originally published in <a href="https://medium.com/netflix-techblog">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 Evolution of Cassandra Data Movement at Netflix]]></title>
            <link>https://netflixtechblog.medium.com/the-evolution-of-cassandra-data-movement-at-netflix-6e13329c80a1?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/6e13329c80a1</guid>
            <category><![CDATA[cassandra]]></category>
            <category><![CDATA[icebergs]]></category>
            <category><![CDATA[big-data]]></category>
            <category><![CDATA[data-movement]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Mon, 18 May 2026 20:45:38 GMT</pubDate>
            <atom:updated>2026-05-18T20:45:38.101Z</atom:updated>
            <content:encoded><![CDATA[<p>By <a href="https://www.linkedin.com/in/guilhermesmi/">Guil Pires</a>, <a href="https://www.linkedin.com/in/jenjprince/">Jennifer Prince</a>, <a href="https://www.linkedin.com/in/josecamachof/">Jose Camacho</a>, <a href="https://www.linkedin.com/in/kenkurzweil/">Ken Kurzweil</a>, <a href="https://www.linkedin.com/in/phanindra-chunduru/">Phanindra Chunduru</a></p><h3>Background</h3><p>In a previous post, we introduced <a href="https://netflixtechblog.medium.com/data-bridge-how-netflix-simplifies-data-movement-36d10d91c313">Data Bridge</a>, a unified management plane for batch Data Movement at Netflix. Historically, several bespoke Data Movement connectors were developed across different engineering organizations to fulfill their specific requirements. Over the last few years, the Data Movement team has started centralizing these offerings through an abstraction that provides a catalog of connectors, along with simple UI and APIs to initiate Data Movement jobs.</p><p>One such case is the Cassandra to Iceberg connector. Apache Cassandra powers mission critical applications at Netflix, including Member, Billing, Recommendations, Subscriptions and many more. These use cases heavily leverage Data Movement to Apache Iceberg for many analytics and operational tasks, and central to this movement was a connector for Cassandra to Iceberg built in-house named Casspactor. As many Cassandra based Data Abstractions emerged, such as <a href="https://netflixtechblog.com/introducing-netflixs-key-value-data-abstraction-layer-1ea8a0a11b30">Key Value</a>, <a href="https://netflixtechblog.com/introducing-netflix-timeseries-data-abstraction-layer-31552f6326f8">Time Series</a> and <a href="https://netflixtechblog.medium.com/high-throughput-graph-abstraction-at-netflix-part-i-e88063e6f6d5">Graph</a> — the need for larger and more complex Data Movement with transformations became more critical to the business.</p><p>Data movements are fundamentally fulfilled by leveraging the existing Cassandra backup infrastructure. Regularly scheduled backups are performed directly on the Apache Cassandra nodes, via a sidecar process managing the upload of all necessary SSTables and associated Metadata files directly into Amazon S3. When a Data Movement job is initiated, the job constructs the specific backup structure it needs by referencing the S3 based metadata, allowing it to precisely locate the SSTable files. The engine then downloads these files, performs the required mutation compaction and processing, and finally writes the fully transformed, compacted data directly into the target Apache Iceberg tables.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DPkj0pP-y0TntplMdm224Q.png" /><figcaption>Image 1: Cassandra Cluster Backups to S3</figcaption></figure><h3>Casspactor: The Engine We Outgrew</h3><p>Casspactor processed roughly 1,200 data movements per day, transferring approximately 3 PB of data from Apache Cassandra into Apache Iceberg tables. It served some of the most critical workloads at Netflix. For years, it worked. Then, two compounding challenges made it clear we needed a fundamentally different architecture.</p><h3>Fragile Metadata Dependencies</h3><p>Before Casspactor could move a single record, it needed to answer a deceptively simple question: <em>which backup exists, is it complete, and what does it contain?</em></p><p>Casspactor assembled this answer from multiple independent systems:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ppVlrVcA2a3zOu0R6PtuUA.png" /><figcaption>Image 2: Casspactor’s Composite View of a Backup</figcaption></figure><p>Each system had its own failure modes, update cadences, and accuracy guarantees. Casspactor’s view of the world was a composite, and composites diverge from reality.</p><p>Metadata fell out of sync with actual backups, causing Casspactor to read stale or incorrect data silently. Routine maintenance on the Cassandra Clusters triggered uncoordinated snapshots, and because Casspactor required all nodes in a region to snapshot at the same clock second, a single node replacement could break data movement for an entire region.</p><p>The fix was hiding in plain sight. The answer to “which backup exists and is it complete?” already lived in the backup storage layer (Amazon S3) itself. By reading metadata directly from the backup files, we could replace the entire dependency chain with a single source of truth.</p><h3>Every Connector Inherited Casspactor’s Limitations</h3><p>Cassandra at Netflix does not just store raw tables. It backs higher level data abstractions, such as <a href="https://netflixtechblog.com/introducing-netflixs-key-value-data-abstraction-layer-1ea8a0a11b30">Key Value</a>, <a href="https://netflixtechblog.com/introducing-netflix-timeseries-data-abstraction-layer-31552f6326f8">Time Series</a>, and others, each with its own data model, access patterns, and semantics. When any of these abstractions needed to move data to Iceberg, they all funneled through Casspactor.</p><p>Every abstraction inherited Casspactor’s constraints:</p><ul><li><strong>Skewed partition failures:</strong> Casspactor could not handle tables with large partitions, a common pattern in Key Value and Time Series workloads. Jobs crashed with out-of-memory errors on some of Netflix’s largest datasets.</li><li><strong>No data model awareness</strong>: Casspactor moved raw Cassandra tables as is. Connectors for Key Value and other abstractions had to bolt on post processing to reconstruct their data models from the raw output — extra cost, extra complexity, and an extra surface for failures.</li><li><strong>Intermediate table bloat</strong>: Casspactor wrote to an intermediate Iceberg table before producing the final output. The Key Value connector added another intermediate table and a snapshots table. Connectors for abstractions on top of Key Value added even more. This compounded into significant storage cost overhead.</li><li><strong>Inability to Time Travel</strong>: by relying on multiple services to compose a backup unit, Casspactor was unable to restore prior backups in the event of cluster Topology or Keyspace schema changes.</li><li><strong>Monolithic design</strong>: Casspactor was built as a single connector, not as an engine. There was no way to build a family of purpose built connectors on a shared foundation.</li></ul><p>We needed something fundamentally different: an engine that reads directly from backups in S3, produces standard Spark DataFrames, and lets each data abstraction build its own connector with full awareness of its data model. One foundation, many connectors.</p><h3>The New Stack: A Layered Architecture</h3><p>The new architecture, built upon the foundation of Apache Cassandra Analytics and the in-house Move Data framework, represents a fundamental shift toward a layered, purpose-built stack designed for reuse and maintainability. This new engine was conceived with clear separation of concerns, moving away from Casspactor’s monolithic design. The architecture is intentionally layered with the foundation being a core S3 reading capability: the Cassandra Analytics Wrapper, which is built on top of the Open Source Cassandra Analytics with Netflix’s internal backup representation and an S3 Client.</p><p>This layer handles the raw data retrieval from backups, translating it into standard Spark DataFrames. Sitting atop this foundation is a “<strong>Connector Factory</strong>” model, via both Java UDFs and transforms which allows individual data abstractions (Key Value, Time Series, others) to build highly optimized, data model aware connectors that process the generic Spark DataFrames, avoiding the need for complex, expensive, and failure-prone post-processing steps. This layered approach ensures that improvements to the core reading engine benefit all connectors, while the connectors themselves are focused solely on data transformation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EwqgicjfpASkWIf3nz4q6A.png" /><figcaption>Image 3: The new Connector layered stack</figcaption></figure><ul><li><strong>Handles Skewed Partitions:</strong> By moving the mutation compaction and processing to the Executor level within Spark, the new engine can efficiently handle tables with highly skewed or wide partitions, a major pain point for Casspactor. Crucially, this processing occurs without excessive data shuffling, preventing out-of-memory errors and enabling reliable movement of Netflix’s largest datasets.</li><li><strong>Operates at Spark DataFrames (No Intermediary Tables)</strong>: The new architecture directly generates standard Spark DataFrames from the Cassandra backups. This eliminates the need for Casspactor’s costly, multi-stage intermediate Iceberg tables, which led to storage bloat and operational complexity. This native DataFrame operation enables the “Connector Factory” by providing a universal, easily consumable interface for building diverse, model specific connectors.</li><li><strong>Jobs Auto Size:</strong> The engine integrates intelligent auto-sizing capabilities, allowing jobs to dynamically adjust resource consumption based on the source table’s characteristics. This removes the burden of manual tuning from engineering teams, ensuring optimal performance and cost efficiency without sacrificing reliability.</li><li><strong>Reduced Dependencies</strong>: By reading metadata directly from the backup files stored in S3, the new stack removes the fragile, multi-service dependency chain that plagued Casspactor. S3 becomes the single, authoritative source of truth for backup existence and completeness, vastly improving data movement reliability and consistency.</li><li><strong>Time Travel</strong>: A critical feature of the new stack is the ability to process the <strong>schema, cluster topology, and data as a cohesive unit </strong>at a specific point in time. This capability provides robust time travel functionality, essential for auditing, debugging, disaster recovery and reproducing past data states.</li><li><strong>Performance</strong>: Collectively, these architectural improvements, including native DataFrame processing, optimized partition handling, and streamlined metadata retrieval have resulted in notable performance gains, reducing overall data movement execution runtime and cost compared to the legacy Casspactor system.</li><li><strong>Cost</strong>: by eliminating intermediary Iceberg tables and efficient SSTable compaction on Executors, the new stack needs a significantly smaller storage and compute footprint leading to significant cost savings in the order of USD millions.</li></ul><h3>The Journey Towards a Safe Migration</h3><p>The successful validation of the new stack was the critical first step, but it only marked the beginning of the most challenging phase: the migration. Large scale data migrations are inherently complex, high-risk undertakings that can be time consuming and often result in customer frustration and service disruption. To navigate the high stakes of decommissioning a mission-critical system like Casspactor and seamlessly replacing it, we needed a strategy that prioritized reliability and transparency above all else.</p><p>The migration was fundamentally enabled by a <strong>Like-for-Like</strong> strategy, which served as the cornerstone of our Platform Engineering philosophy, abstracting complexity. The core tenet was to maintain absolute consistency across the user-facing interface, the output contract, and the final data artifact. This meant ensuring that the data movement parameters defined via the Data Bridge abstraction remained unchanged, and, critically, the schema, metadata, and data within the destination Iceberg tables were identical to the legacy output. By preserving these external contracts, we eliminated the need for complex, time-consuming coordination with dozens of internal teams who relied on these data pipelines. This approach transformed the migration from a distributed, high-risk, multi-team effort into an internal platform implementation detail, allowing us to achieve a transparent, zero-impact transition and accelerate the retirement of the legacy system without requiring any code changes or validation from downstream users.</p><p>To navigate this migration, we developed a strategy anchored by three core pillars that serve as a blueprint for successful, large-scale data migrations:</p><ol><li><strong>Validation</strong>: Establishing and maintaining absolute confidence in data consistency through rigorous, ongoing validation.</li><li><strong>Visibility</strong>: Instrumenting every part of the system to provide a clear, real-time understanding of migration progress and system health.</li><li><strong>Safety</strong>: Ensuring user impact is minimized or eliminated, despite the inevitable system failures, by leveraging abstractions and robust fallbacks.</li></ol><p>The next section will provide a detailed exploration of these key pillars.</p><h3>Pillar 1: Validation</h3><p>Trust is earned, and in data migration, it is earned one row at a time. The first pillar is the most critical: providing a measurable guarantee to users and partners that the data produced by the new system is an exact, row-by-row replica of the data produced by the old one.</p><p>Our foundational tactic was deploying the new Move Data connector in a “shadow” testing that ran in parallel with the production Casspactor jobs. This allowed us to validate the new system with real-world, production workloads without any customer impact.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*L8yxnLU-7YqzHeEAmVRv6g.png" /><figcaption>Image 4: Shadow job structure leveraged for data validation</figcaption></figure><ul><li>Let <strong>C</strong> be the set of rows in the legacy Casspactor output (Iceberg table).</li><li>Let <strong>M</strong> be the set of rows in the new Move Data output (Iceberg table).</li></ul><p>The test for trust: prove that <strong>C = M</strong>. This required continuously checking for two conditions:</p><ol><li>Rows in <strong>C</strong> but not in <strong>M</strong> (<em>C-M</em>): The new system missed data.</li><li>Rows in <strong>M</strong> but not in <strong>C</strong> (<em>M-C</em>): The new system introduced phantom or erroneous data.</li></ol><p>Any result where the cardinality of these difference sets (the number of differing rows) was greater than zero triggered an immediate, high-priority investigation. The target was 100% similarity.</p><h3>Uncovering and Resolving Disparities</h3><p>The shadow mode quickly became a powerful forensic tool, exposing “unknown unknowns”, subtle discrepancies that were not bugs in the new system but rather differences in behavior between the new and old systems. Resolving these was the core work of building trust. For each problem we initiated an investigation log where we captured the details, logs, queries that allowed us to diagnose. Based on the assessment the issues were categorized so that similar differences on other datasets were later resolved affecting many of the shadow pipelines.</p><p>Maintaining an investigation log was critical to organize the outstanding issues and effectively communicate to stakeholders the progress and confidence of the new connector so that we effectively measure the appropriate level of “confidence” to initiate the migration.</p><p>We observed differences in how connectors leverage reference timestamps for Time-to-Live, Consistency Levels, backup selection, and various internal business logic. This continuous, data-driven cycle of discovery and resolution was the mechanism by which we built confidence in the new architecture.</p><h3>Pillar 2: Visibility</h3><p>Trust is built in the background, but an active migration requires real-time insight: Visibility. The second pillar involves instrumenting the system to provide an unambiguous, clear understanding of operational health and migration progress.</p><p>We extended our instrumentation to the overall migration workflow and its dependencies:</p><ul><li><strong>Dashboards</strong>: We created centralized dashboards to track migration status, visualizing the total number of data movements migrated versus those remaining. The dashboards tracked execution status, average runtime, and cost comparisons between the two connectors.</li><li><strong>Dependency Tracking</strong>: Since the new system relied on a new set of APIs to fetch backup metadata, we implemented detailed metrics for failures to keep track of the APIs or dependencies failed.</li><li><strong>Alerting</strong>: Proactive alerts were set up for job failures (Move Data or Casspactor), failures on Move Data that triggered a fallback to Casspactor or any data discrepancy being detected.</li></ul><p>This comprehensive instrumentation allowed the team to be proactive, fix issues as they emerged during the migration, and gain the necessary confidence to accelerate the migration timeline.</p><h3>Pillar 3: Safety</h3><p>Even with perfect data correctness and enhanced visibility, the third pillar, Safety is required for a zero-impact migration. The challenge is ensuring that when a system inevitably fails, the user experience is uninterrupted. Our strategy centered on decoupling the user’s workflow from the underlying connector implementation.</p><h3>Leveraging Abstraction: The Decider Pattern</h3><p>To achieve a transparent swap, we leveraged the <a href="https://github.com/Netflix/maestro?tab=readme-ov-file">Maestro</a> workflow orchestration platform to implement the Decider pattern:</p><ol><li><strong>Data Movement Abstraction:</strong> From a user’s perspective, their Data Movement job definition remained the same.</li><li><strong>The Decider Step</strong>: Internally the workflow responsible to execute the job was modified to include a Decider step. This step took the data movement parameters (source cluster, table name, destination) and invoked a control plane: Connector Controller.</li><li><strong>Connector Controller as the Registry</strong>: The control plane served as the dynamic registry. Based on the migration cohort and the data movement attributes, it determined and reported the appropriate connector to use either Casspactor (legacy) or Move Data (new).</li></ol><p>This abstraction gave our team complete control. We could upgrade or rollback any connector for any data movement instantly by simply updating a configuration in the controller, with zero modification required to the thousands of downstream customer workflows. Crucially, this abstraction guaranteed the critical safety net: a conditional step in the Maestro workflow logic ensured that if the Move Data step fails, it would immediately execute the Casspactor step.</p><p>This pattern would increase the chances that the user’s data movement completes successfully, even if the new connector encountered a bug or transient failure during the initial rollout phases. User impact was completely eliminated; they might see a slightly longer runtime in the event of a failure and fallback, but they would never see a migration failure or suffer from stale data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1Qq5sgvtKuLfpP2byfggMw.png" /><figcaption>Image 5: The Decider Pattern Implementation via Maestro</figcaption></figure><p>Beyond the workflow, the new system architecture itself was inherently more resilient. By building the new data movement connector on Cassandra Analytics and reading backups directly from S3, we removed fragile dependencies on deprecated internal services.</p><h3>Conclusion</h3><p>The migration from Casspactor to the new, layered architecture built on Cassandra Analytics and the Move Data connector was more than a typical “tech debt” project; it was a fundamental shift in our approach to data movement reliability and scalability at Netflix.</p><p>The legacy system, while serving us well for years, was ultimately constrained by monolithic design, fragile metadata dependencies, and an inability to handle the complexity of modern data abstractions. The new stack resolves these issues by delivering a robust, cost-efficient, and inherently more resilient solution that reads directly from S3, handles wide partitions gracefully, and eliminates costly intermediate tables.</p><p>Our blueprint for the migration, anchored by the three pillars of Validation, Visibility, and Safety, ensured a transparent and high-confidence transition. Through rigorous shadow testing and a data-driven audit framework, we achieved the desired data consistency. Enhanced dashboards and alerting provided the real-time operational insight necessary to manage risk. Most critically, the implementation of the Decider pattern within our workflow abstraction minimized the impact for all downstream users.</p><p>This successful migration validates a core philosophy: by abstracting complexity at the platform level, we can perform large system migrations without burdening our product engineering partners. The new foundation is now ready to support the next generation of Netflix’s data abstractions.</p><h3>Looking ahead</h3><p>This foundational work on the Cassandra Data Movement stack has done more than just replace a legacy system: it has become an accelerator for innovation across the entire Data Movement organization. By providing a reliable, performant engine that standardizes data retrieval into Spark DataFrames, we’ve enabled the rapid development of new, highly optimized connectors. This new “Connector Factory” approach has already delivered a dedicated Key-Value to Iceberg and Time Series connectors, both of which are fully aware of their respective data models, eliminating costly post-processing. This architecture is also paving the way for ambitious new initiatives, including the development of a solution for bulk loading data into Cassandra itself, effectively completing the data movement cycle, and enabling safer fleetwide connector rollout with canaries inspired by the Decider Pattern.</p><p>We are incredibly grateful for the extensive collaboration among the Data Movement, Data Bridge, Online Data Stores, Membership, Billing, Subscriber and Ads platform teams at Netflix; this work simply couldn’t have been accomplished without their partnership!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6e13329c80a1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Data Projects: Managing Data Assets at Netflix Scale]]></title>
            <link>https://netflixtechblog.medium.com/data-projects-managing-data-assets-at-netflix-scale-7ca25888591e?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/7ca25888591e</guid>
            <category><![CDATA[big-data]]></category>
            <category><![CDATA[data-governance]]></category>
            <category><![CDATA[data-orchestration]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Mon, 11 May 2026 23:35:11 GMT</pubDate>
            <atom:updated>2026-05-11T23:35:11.317Z</atom:updated>
            <content:encoded><![CDATA[<h4><em>By </em><a href="https://www.linkedin.com/in/amer-hesson-0886a5a5/"><em>Amer Hesson</em></a><em>, </em><a href="https://www.linkedin.com/in/mayworm/"><em>Marcelo Mayworm</em></a><em>, </em><a href="https://www.linkedin.com/in/james-mulcahy-10493518/"><em>James Mulcahy</em></a><em>, and </em><a href="https://www.linkedin.com/in/brittany-truong-a35b54bb/"><em>Brittany Truong</em></a></h4><h3>The Problem: Managing Assets at Netflix Scale</h3><p>Netflix’s Data Platform is vast. We have millions of tables in our data warehouse and tens of thousands of scheduled workloads running across our orchestration systems. Behind each of these assets sits an engineer, a team, or an initiative — and behind each of those sits a set of decisions about <em>who</em> can access <em>what</em>, and <em>how</em> those workloads execute day after day.</p><p>For years, the tools we used to manage access and identity for these assets operated at the granularity of the individual asset. Every table had its own Access Control List (ACL). Every workflow ran under the identity of the engineer who authored it. In a workforce that is fluid, where people change teams, change roles, and occasionally leave the company, this fine-grained model broke down in two persistent, painful ways.</p><h3>Problem 1: Permissions that can’t keep up with organizational changes</h3><p>Imagine you’re on a team that owns a few hundred tables. Your org restructures, a neighboring team merges into yours, and you inherit another few hundred. Now you have to find every ACL on every table, figure out who should still have access, and update them one by one. Multiply that by every reorg across every team across the company. The result? Two failure modes:</p><ol><li><strong>The support team gets flooded.</strong> A significant and outsized share of support threads were requests to update table permissions en masse in response to org changes. While self-service tooling and best practices are in place to manage this, adherence is inconsistent. Data Projects addresses this by promoting the solution from optional tooling to a foundational part of the data platform.</li><li><strong>Access gets granted far too broadly.</strong> Rather than maintain fine-grained ACLs, teams would often open up table access to the whole company. This defeated the purpose of having ACLs in the first place.</li></ol><h3>Problem 2: Workloads tied to human identities</h3><p>Scheduled and asynchronous workloads — <a href="https://netflixtechblog.com/maestro-netflixs-workflow-orchestrator-ee13a06f9c78">Maestro</a> workflows, data movement jobs, Spark pipelines — need an identity to run as. Historically, that was a <em>human</em>: whoever authored the workflow.</p><p>Human identities are not durable. People change teams, get new responsibilities, and leave the company. When they do, their permissions change, and the workflows running under their identity start to fail. The only fix was to swap in a colleague’s identity, which inevitably had <em>different</em> permissions, kicking off a “permissions whack-a-mole” as each fix surfaced the next missing grant. And then, eventually, that colleague would also move on, and the cycle would repeat.</p><h3>Enter Data Projects</h3><p>We introduced Data Projects to tackle both problems head-on. At its core, a Data Project is two things:</p><ol><li><strong>A container to manage and view a set of related assets in aggregate</strong>: tables, workflows, and other data assets grouped under a single logical umbrella.</li><li><strong>A synthetic, durable, and assumable identity</strong>: one that asynchronous and scheduled workloads can execute under, independent of any human’s lifecycle.</li></ol><p>You can think of it as hoisting the granularity of management up from the individual asset to a meaningful container: the <em>project</em>. Instead of managing permissions on 500 tables, you manage them on one project that contains those 500 tables.</p><p>While the initial focus has been access and identity, the abstraction has applications well beyond those concerns. That broader potential is part of what makes it worth investing in.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Te9vjGxhHK7jMmpSO6sy2Q.png" /><figcaption><strong>Figure 1a</strong>. Individual assets, each managed in isolation, with per-asset access controls and per-person ownership.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*u6MJRtRFedXQ0w3QCIbqBA.png" /><figcaption><strong>Figure 1b</strong>. These assets are logically grouped into projects for easier management.</figcaption></figure><h3>Grants and Roles</h3><p>Each Data Project has a set of grants managed by the owning team. Different identity types can be added as grants: users, groups, applications, and continuous integration (CI) jobs. Each grant has a role that determines what the grantee can do within the project. For example, a <em>Contributor</em> has read/write access to the project’s assets, while a <em>Viewer</em> has read-only access. These roles roll up neatly — instead of rewriting hundreds of ACLs when someone joins or leaves a team, you update a single project grant.</p><h3>The Identity Umbrella: Netflix and IAM</h3><p>Every Data Project is provisioned with a Netflix application identity, and optionally an AWS IAM role. This is the “identity umbrella” that makes workloads durable:</p><ul><li>The project’s <strong>Netflix identity</strong> is what executes the project’s async workloads (e.g. Maestro workflows). It belongs to the project, not to any person.</li><li>The project’s <strong>IAM role</strong> supports specialized use cases in AWS like Spark jobs on Amazon EMR. Crucially, the IAM role can be exchanged for the project’s Netflix identity in a cryptographically secure way.</li></ul><p>Members with privileged roles can also assume the project’s Netflix identity. This is enormously useful for testing and troubleshooting from a development context like a laptop or a notebook — you get to run commands <em>as the project</em>, exactly as the scheduled workload would.</p><h3>Gravity</h3><p>One of the more elegant properties of Data Projects is what we call <em>gravity</em>. When a workload running under a project’s identity creates a new asset — say a Maestro workflow creates three tables — those assets are automatically added to the project as contained assets. The project becomes the center of mass for everything produced under its identity. You get organization for free as a side effect of how the platform already works, eliminating future challenges of discovering relevant assets and gaining access to them.</p><h3>Securing Data Workflows with Data Projects</h3><p>Maestro is Netflix’s primary workflow orchestrator for batch analytics, covering scheduled ETL pipelines, data movement jobs, ML training, and much more. Because workflows can run on schedules without the original user present, Maestro is designated a Trusted Workload Manager (TWM), formally authorized to mint fresh identity tokens on behalf of the workloads it manages.</p><p>That identity matters everywhere. A single workflow execution may be checked against table ACLs in the Secure Data Warehouse, authorization policies for Netflix resources, and IAM policies for AWS — all in a single run. If the identity is fragile, the whole workflow is fragile.</p><h3>The Problem with User-Tied Identity</h3><p>The standard pattern was to run workflows under an On-Behalf-Of (OBO) credential — for example, <em>maestro</em> OBO <em>alice@netflix.com</em>. This gave the workflow the union of Maestro’s and the human’s permissions, but in doing so it also bound the workflow’s permissions to that person’s. When they changed teams or left Netflix, the workflow broke. A colleague might take over ownership, but they rarely had the same access as the previous owner, so the workflow would stay broken for days while permissions were sorted out. At Netflix’s scale, with tens of thousands of scheduled workloads, many of them business-critical, this was unsustainable.</p><h3>Data Projects: Durable Identity</h3><p>Data Projects solves this by replacing user-tied identity with a durable, team-owned Netflix application identity: one that doesn’t change teams, go on vacation, or leave the company. Each project groups related workflows, tables, secrets, and other assets under a single consistent identity, and Maestro validates the caller’s access to the project before executing any workflow under it.</p><p>The downstream improvements are as follows:</p><ul><li>Tables created during execution are automatically associated with the project’s identity through <em>gravity</em>, inheriting its access controls without additional configuration.</li><li>Secrets are scoped to project policies, so ownership transfers no longer strand credentials.</li><li>Access is managed once at the project level, replacing fragmented per-user grants across every asset the workflow touches.</li></ul><p>The result is a workflow identity model that is stable, auditable, and built to survive the organizational changes inevitable at any company operating at this scale.</p><h3>Success Stories</h3><p>Many Data Projects have already grown to contain tens of thousands of assets in production. A couple examples are highlighted below:</p><ul><li><strong>Streaming Quality of Experience</strong>: A core observability pipeline tracking quality of experience (QoE) metrics whose continuity used to depend on whichever engineer happened to own the underlying workflows. Now it runs under the project’s identity, stable regardless of team membership changes.</li><li><strong>Member Analytics</strong>: Analytical models and ETL workflows for member data products. A concentrated set of business-critical analytics whose access is managed at the project level rather than across hundreds of individual tables and workflows.</li></ul><p>More broadly, we’ve seen Data Projects adopted as the organizing principle for entire analytics domains. Where teams previously maintained their own access policies, ad-hoc grant lists, and tribal knowledge about “who should have access to what,” the project is now the single answer.</p><h3>Using Data Projects</h3><p>Onboarding workflows onto Data Projects is a matter of:</p><ol><li>Creating a project for the logical grouping of assets (or using an existing suitable one).</li><li>Granting the right people and groups the appropriate roles.</li><li>Configuring the workflow to run with the project’s identity.</li></ol><p>Thanks to gravity, new assets produced by project workflows land in the project automatically. Migrating existing workflows can be a challenge as it requires setting up the Data Project with the appropriate permissions before changing its execution identity. We are actively working on infrastructure to track the access patterns of existing workflows so that we can recommend precise permission updates for the destination project. Our goal is to make the Data Project the de facto option for executing any kind of asynchronous workload.</p><h3>What’s Next</h3><p>Data Projects started as an Analytics Platform initiative, a response to specific pains in the data warehouse, but the underlying ideas are not unique to data. We see a potential future where <strong>Projects</strong> (not just <em>Data</em> Projects) are a first-class platform concept spanning data assets, software assets (GitHub repositories, Spinnaker applications, Docker images), and even studio assets (production content, pipelines, and transformations).</p><p>We’re also investing in:</p><ul><li><strong>Rightsizing</strong>: we’re integrating a layer on top of our authorization policies that automatically rightsizes permissions based on actual usage patterns, proactively eliminating unnecessary access and preventing “permission creep”.</li><li><strong>Hoisting beyond access and identity</strong>: the project is a natural unit for surfacing other concerns at the aggregate level — cost attribution, health indicators, and more.</li><li><strong>Ad-hoc use case integrations</strong>: extending project identities beyond scheduled workloads to cover interactive, on-demand actions like running a query through the Data Portal.</li><li><strong>Activity logs and audits</strong>: a unified timeline of grant changes, asset changes, and workflow versions at the project level.</li></ul><h3>Conclusion</h3><p>Data Projects is an answer to a simple observation: at Netflix’s scale, the unit of identity and access management can’t be the individual asset or the individual human. It has to be something larger, something durable, something that matches the way teams actually think about the work they own.</p><p>A project is that unit. And as we continue to generalize the concept beyond the data warehouse, we expect it to become one of the foundational primitives of how engineering at Netflix is organized, not just how data is organized.</p><h3>Acknowledgments</h3><p>We would like to express our gratitude to the following individuals for their contributions to this effort: Ryan Bordo, Doug Clark, Luke Fernandez, Sarrah Figueroa, Ankit Gupta, Brian Hoying, Ye Ji, Abhishek Kapatkar, Anmol Khurana, Matheus Leão, Hechao Li, Raymond Liu, Alice Naghshineh, David Noor, Anjali Norwood, Javier Garcia Palacios, Kunaal Parekh, Brandon Quan, Andrew Seier, Jason Seo, and Ethan Zhang.</p><p>If you are interested in helping us solve these types of problems and helping entertain the world, please take a look at some of our open positions on the <a href="https://jobs.netflix.com/">Netflix jobs page</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7ca25888591e" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Scaling ArchUnit with Nebula ArchRules]]></title>
            <link>https://medium.com/netflix-techblog/scaling-archunit-with-nebula-archrules-b4642c464c5a?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/b4642c464c5a</guid>
            <category><![CDATA[gradle]]></category>
            <category><![CDATA[nebula]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[archunit]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Fri, 08 May 2026 15:01:00 GMT</pubDate>
            <atom:updated>2026-05-08T15:56:00.919Z</atom:updated>
            <content:encoded><![CDATA[<p>By <a href="https://github.com/wakingrufus">John Burns</a> and <a href="https://www.linkedin.com/in/emilyyuan03/">Emily Yuan</a></p><h3>Introduction</h3><p>At Netflix, we operate using a <a href="https://netflixtechblog.com/towards-true-continuous-integration-distributed-repositories-and-dependencies-2a2e3108c051">polyrepo</a> strategy with tens of thousands of Java repositories. This means that we need to have ways of sharing common build logic across these repositories. On the <a href="https://sites.google.com/netflix.com/javaplatformnetflix/jvm">JVM Ecosystem team</a> within Java Platform, we build tooling such as the <a href="https://github.com/nebula-plugins">Nebula suite of Gradle plugins</a> to provide standard ways to build projects, keep dependencies up-to-date, and publish artifacts reliably across the Java ecosystem. Our mission also entails providing build-time feedback to the developer when they deviate from the <a href="https://netflixtechblog.com/how-we-build-code-at-netflix-c5d9bd727f15">paved road</a>, or when their code base contains technical debt.</p><h3>Case Study</h3><p>After a Netflix incident relating to a library releasing a backwards-incompatible change, our team was asked to provide some tooling and practices to improve the Java library lifecycle management. This was not a simple case of a library making a reckless breaking change. The code removed had been deprecated for years. Library authors often struggle to know when it is safe to remove deprecated code, or refactor code that is not meant to be used by downstream applications. Fleet-wide migrations, such as upgrading major Spring Boot versions, also involve deprecated code removal. To help with this, we established a suite of API lifecycle annotations:</p><ul><li>@Deprecated from the Java standard library</li><li>@Public A custom annotation to use on APIs meant to be used downstream</li><li>@Experimental A custom annotation for new APIs which may not yet be stable</li><li>All other APIs are assumed to be “internal”</li></ul><p>Library authors can annotate their APIs with these annotations. However, how will they know which downstream projects are using their API incorrectly, based on these?</p><p>As we sought to improve the paved road for JVM-based libraries at Netflix, we needed a good way of identifying this kind of technical debt, not only for the benefit of the Java Platform-provided libraries, but any team delivering shared libraries to the organization. For this, we looked at ArchUnit.</p><p><a href="https://www.archunit.org/">ArchUnit</a> is a popular OSS library (3.5k stars, 84 contributors) used to enforce “architectural” code rules as part of a JUnit suite. It is used internally by Gradle, Spring, and is provided as part of the <a href="https://spring.io/projects/spring-modulith">Spring Modulith</a> platform. The rules engine, which is built directly on top of <a href="https://asm.ow2.io/">ASM</a>, can be used for a wide variety of use cases. It is powerful enough to be a general purpose static analysis tool with the following distinctive features:</p><p>1. Works cross-language (JVM), because it uses ASM/bytecode, not AST parsing.</p><p>2. Exposes a builder API pattern that makes it easy to write rules</p><p>3. Also has a lower level API ideal for writing more complex custom rules.</p><p>The limitation of ArchUnit is that it is designed to be used as part of a JUnit suite in a single repository. The Nebula ArchRules plugins give organizations the ability to share and apply rules across any number of repositories. Rules can be sourced from OSS libraries or private internal libraries. This makes the plugin generally useful for any JVM+Gradle engineering organization.</p><h3>Why ArchUnit?</h3><p>Before we go into how ArchRules works, it is good to understand why we would want to use ArchUnit in this way instead of other static analysis tools.</p><h4>AST vs Bytecode</h4><p>Some tools, such as PMD, process rules against an AST (abstract syntax tree). An AST is a structured representation of source code. This kind of tool will have rules that are syntax dependent. Rules that need to support multiple JVM languages, such as Kotlin or Scala, often need to be rewritten for each language. It also allows code which should be found to be hidden under syntactic sugar not anticipated by the rule author. ArchUnit uses <a href="https://asm.ow2.io/">ASM</a> to analyze actual compiled bytecode, which means it doesn’t matter how that code was produced. What is analyzed is the actual code that will be run.</p><h4>Rule Authorship</h4><p>Tools like PMD and Spotbugs are not optimized for custom rule authorships. Most usage of these tools run built-in provided rules, or add in pre-made third party plugins. Take a look at what a custom rule for PMD might look like:</p><pre>&lt;![CDATA[<br> //AllocationExpression/ClassOrInterfaceType[<br>   @Image=&#39;DateTime&#39; and (<br>       (count(..//Name[@Image=&#39;DateTimeZone.UTC&#39;])&lt;=0)<br>       and<br>       (count(..//Name[@Image=&#39;DateTimeZone.forID&#39;])&lt;=0)<br>    ) or (<br>       (<br>           (count(..//Name[@Image=&#39;DateTimeZone.UTC&#39;])&gt;0)<br>             or<br>           (count(..//Name[@Image=&#39;DateTimeZone.forID&#39;])&gt;0)<br>       ) and (../Arguments/ArgumentList and count(../Arguments/ArgumentList/Expression) = 1)<br>   )<br> ]<br>]]&gt;</pre><p>This rule ensures that DateTimes are not instantiated without an explicit zone. This is a raw string meant to be used within PMD’s xpath parser. There is no IDE guidance on crafting it. To test it, a whole separate PMD process needs to be wired up to interpret the rule and evaluate it against a source file. Let’s see how a similar rule would look with ArchUnit:</p><pre>ArchRuleDefinition.priority(Priority.MEDIUM)<br>.noClasses()<br>.should()<br>.callConstructorWhere(<br>    // constructor does not have a zone arguement<br>    target(doesNot(have(rawParameterTypes(DateTimeZone.class))))<br>   // constructor is for DateTime<br>        .and(targetOwner(assignableTo(DateTime.class)))<br>)</pre><p>This is type-safe Java code with a fluent API. It is also simple to unit test, as ArchUnit has a method to pass a rule object and class references to evaluate the rule against those classes.</p><h4>Class Relations</h4><p>Because ArchUnit processes the entire classpath with ASM, it retains a graph of the class data, allowing rules to easily traverse class relationships and call sites. This allows rules to have much more context about the code it is evaluating.</p><h3>Rules Libraries</h3><p>The first step was to build the ability to write ArchUnit rules which can be shared and published. In order to do this, we have the <a href="https://github.com/nebula-plugins/nebula-archrules-plugin?tab=readme-ov-file#authoring-rules">ArchRules Library Plugin</a>. This plugin adds an additional source set to your Gradle project called archRules. In this source set, you can create a class which implements the ArchRulesService interface. This interface has a single abstract method which returns a Map&lt;String, ArchRule&gt;. The keys of this map are the names of your rules, and the ArchRule is the rule you would like to define using the standard ArchUnit API. Here is an example:</p><pre>public class GuavaRules implements ArchRulesService {<br>  static final ArchRule OPTIONAL = ArchRuleDefinition.priority(Priority.MEDIUM)<br>        .noClasses()<br>        .should()<br>        .dependOnClassesThat()<br>        .haveFullyQualifiedName(&quot;com.google.common.base.Optional&quot;)<br>        .because(&quot;Java Optional is preferred over Guava Optional&quot;);<br><br>    @Override<br>    public Map&lt;String, ArchRule&gt; getRules() {<br>        Map&lt;String, ArchRule&gt; rules = new HashMap&lt;&gt;();<br>        rules.put(&quot;guava optional&quot;, OPTIONAL);<br>        return rules;<br>    }<br>}</pre><p>This code and its dependencies will not be bundled with your main code. It is bundled into a separate Jar with the arch-rules classifier. When publishing, your library will publish this jar as a separate variant with the usage attribute set to arch-rules. This means that in order for downstream projects to use these rules, they must use <a href="https://docs.gradle.org/current/userguide/publishing_gradle_module_metadata.html">Gradle Module Metadata</a> for dependency resolution. There are 2 flavors of rules Libraries: Standalone rules libraries, bundled rule libraries.</p><h4>Standalone Rule Libraries</h4><p>A Standalone Rule library contains no main code: only archRules. These are useful for defining rules for code you don’t own, such as Core Java APIs or OSS libraries. They are also useful for generic rules that can apply to any code, such as “don’t use code marked as @Deprecated”. We maintain a <a href="https://github.com/nebula-plugins/nebula-archrules">collection</a> of OSS Standalone rule libraries which anyone is free to use, and serve as examples of the types of rules you may want to write yourself. However, the real power of ArchRules is in “bundled rule libraries”.</p><h4>Bundled Rule Libraries</h4><p>A bundled rule library is a library with both main and archRules sources. The main source set will contain useful library code, whatever it may be. The archRules will contain rules specific to the usage of that library. For example, rules scoped to that library’s package, or referencing that library’s specific API. Whenever possible, we recommend writing rules in this bundled way. That is because the <a href="https://github.com/nebula-plugins/nebula-archrules-plugin?tab=readme-ov-file#running-rules">ArchRules Runner Plugin</a> will be able to automatically detect these rules and run them in only the source sets that use this library as a dependency. An example of this can be seen in our <a href="https://github.com/nebula-plugins/nebula-test/blob/main/src/archRules/java/com/netflix/nebula/test/archrules/NebulaTestArchRules.java">Nebula Test</a> library.</p><p>In any case, the library plugin will automatically generate a service loader registration entry for your ArchRulesService so that the runner can discover your rules.</p><h3>Running Rules</h3><p>The <a href="https://github.com/nebula-plugins/nebula-archrules-plugin?tab=readme-ov-file#running-rules">ArchRules Runner Plugin</a> allows rules to be evaluated against your code. Standalone rule libraries can be evaluated against all source sets by adding them to the archRules configuration in your build. For example:</p><pre>dependencies {<br>    archRules(&quot;your:rules:1.0.0&quot;)<br>}</pre><p>As mentioned before, bundled rules will be evaluated automatically. To do this, the runner plugin creates a separate configuration for each of your source sets. In each of these configurations, the archRules classpath is combined with the runtimeClasspath with the arch-rules variant selected. This configuration is the classpath used when the ServiceLoader discovers implementations of ArchRulesService. In the following example, we have a Project which uses a test helper library as a testImplementation dependency, and also adds a standalone rules library to the archRules configuration. The test runtime classpath will only contain the implementation jar for the helper library, but the arch rules runtime will contain the archrules jar for the bundled rules and standalone rules. This all happens automatically.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pWrmMaPCSm3XRIsTyMEpgQ.png" /><figcaption>Gradle configurations used by ArchRules</figcaption></figure><p>Once the rules classpath is determined, the runner plugin will create a Gradle work action to evaluate rules against that specific source set. This action runs with classpath isolation using the *archRuleRuntime configuration. Within this action, a ServiceLoader is used to discover rule definitions. The action ends by writing a binary serialization of rule violations to a file for reporting.</p><p>In a project running rules, you also have the ability to customize rule configurations using the archRules extension. For example, you can override a rule’s priority level:</p><pre>archRules {<br>    ruleClass(&quot;com.netflix.nebula.archrules.deprecation&quot;) {<br>        priority(&quot;HIGH&quot;)<br>    }<br>}</pre><p>Other <a href="https://github.com/nebula-plugins/nebula-archrules-plugin?tab=readme-ov-file#running-rules">customizations</a> include disabling running rules on certain source sets and configuring the failure threshold (i.e., high priority failures will cause the build to fail).</p><h3>Reporting</h3><p>The ArchRules runner plugin has two built-in reports: JSON and console. The json report will collect the output from all source sets within a project and create a single json file with all of the data. The console report also collects the output from all source sets within a project, but it prints to the console an easy to read report, for example:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BJ2ONrMNHEFsBEks3WMPaQ.png" /><figcaption>Console Report output</figcaption></figure><p>Note that failure details feature a detailed plain English description, along with a pointer to the exact line of code in violation.</p><p>For custom reporting, you can either use the JSON file, or create your own task that reads the binary files. Take a look at the source code for the ArchRules runner plugin’s report tasks for an example of how to do this.</p><h3>Case Study Solution</h3><p>Going back to our original problem, using ArchRules, we were able to deliver a platform for library authors to track the usage of their APIs. They write ArchRules to detect usage of the annotations, scoped to their library’s package, such as:</p><pre>ArchRuleDefinition.priority(Priority.MEDIUM)<br>    .noClasses().that(resideOutsideOfPackage(packageName + &quot;..&quot;))<br>    .should()<br>    .dependOnClassesThat(resideInAPackage(packageName + &quot;..&quot;).and(are(deprecated())))<br>    .orShould().accessTargetWhere(targetOwner(resideInAPackage(packageName + &quot;..&quot;))<br>        .and(target(is(deprecated())).or(targetOwner(is(deprecated())))))<br>    .allowEmptyShould(true)<br>    .because(&quot;Deprecated APIs are subject to removal&quot;);</pre><p>NB: the deprecated() predicate comes from <a href="https://github.com/nebula-plugins/nebula-archrules/blob/main/archrules-common/src/main/java/com/netflix/nebula/archrules/common/CanBeAnnotated.java">nebula-archrules</a>.</p><p>Our internal Nebula standard Gradle wrapper and plugin suite automatically enable the ArchRules runner on every project, and provides a custom reporter which sends the report data to our Internal Developer Portal on every main-branch CI build. This way, library authors can easily see a report of all downstream consumers using their experimental, deprecated, or non-public APIs, giving them confidence to make “breaking” changes, knowing that it will not actually break downstream consumers. If their changes are currently blocked by downstream usage, they can easily see exactly which projects are reporting those usages.</p><h3>OSS Rule Libraries</h3><p>While the most powerful way to use ArchRules is for you to write your own rules, we have built some <a href="https://github.com/nebula-plugins/nebula-archrules">OSS rule libraries</a> that anyone is free to use, or reference as examples.</p><h4>Nullability</h4><p>These rules enforce proper nullability annotation in Java, for example, that every public class is marked with <a href="https://jspecify.dev/">JSpecify</a>’s @NullMarked. It is smart enough to exclude Kotlin code, as Kotlin has built-in nullability.</p><h4>Gradle Plugin Best Practices</h4><p><a href="https://docs.gradle.org/current/userguide/writing_plugins.html">Writing Gradle plugins</a> can be hard, especially since there are many APIs and patterns that should not be used anymore. These rules help enforce current best practices when writing Gradle plugins.</p><h4>Joda / Guava Rules</h4><p>These rule libraries discourage the use of Joda Time and Guava classes (respectively) as these have been superseded by java.time and standard library enhancements.</p><h4>Security Rules</h4><p>These rules help mitigate CVEs by detecting usage of known vulnerable APIs. Ideally, we keep dependencies up to date to mitigate CVEs. But sometimes that is not immediately feasible, and in those cases, a compile time check to ensure the specific vulnerable API is not used is often good enough.</p><h3>Conclusion</h3><p>We are now running 358 (and counting) rules across over 5,000 repositories detecting over nearly 1 million issues. About 1,000 of these issues are for “High” priority rules. Being able to run these rules on this scale allows us to quickly gain insight into our large fleet of microservices, and identify the areas carrying the most critical technical debt. This makes it easier to focus and prioritize our efforts.</p><p>Going forward, we will be exploring how to tie auto-remediation solutions into the ArchRules findings. ArchUnit currently provides very specific and detailed information about failures in reports, which makes a very strong input signal to an auto remediation tool. We will explore deterministic solutions such as <a href="https://docs.openrewrite.org/">OpenRewrite</a> and non-deterministic solutions such as LLMs. Pairing the easy rule authorship and deterministic results of ArchUnit with an auto-remediation tool that can correctly interpret the results to solve the issue at hand will be a very powerful combination.</p><p>We also will investigate how to get ArchRule failure information surfaced in the IDE as inspections.</p><p>If you have questions or feedback about Nebula ArchRules, reach out to us by posting in the #nebula channel on the <a href="http://gradle-community.slack.com">Gradle Community</a> Slack.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b4642c464c5a" width="1" height="1" alt=""><hr><p><a href="https://medium.com/netflix-techblog/scaling-archunit-with-nebula-archrules-b4642c464c5a">Scaling ArchUnit with Nebula ArchRules</a> was originally published in <a href="https://medium.com/netflix-techblog">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[Democratizing Machine Learning at Netflix: Building the Model Lifecycle Graph]]></title>
            <link>https://medium.com/netflix-techblog/democratizing-machine-learning-at-netflix-building-the-model-lifecycle-graph-5cc6d5828bb1?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/5cc6d5828bb1</guid>
            <category><![CDATA[mlops]]></category>
            <category><![CDATA[event-driven-architecture]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <category><![CDATA[knowledge-graph]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Mon, 04 May 2026 16:01:02 GMT</pubDate>
            <atom:updated>2026-05-15T18:44:41.436Z</atom:updated>
            <content:encoded><![CDATA[<p><a href="https://www.linkedin.com/in/saishsali/">Saish Sali</a>, <a href="https://www.linkedin.com/in/nipunk/">Nipun Kumar</a>, <a href="https://www.linkedin.com/in/suraelamurugu/">Sura Elamurugu</a></p><h3>Introduction</h3><p>As Netflix has grown, machine learning continues to support our ability to deliver value to members and drive excellence across multiple areas of our business. When Netflix began investing in machine learning over a decade ago, it was primarily focused on a single domain: personalization. Scala was the industry standard, our ML teams were relatively small, and optimizing member engagement was our primary use case. Fast forward to today, and machine learning has become the backbone of Netflix’s business transformation. We now apply ML across various business domains, including:</p><ul><li><strong>Personalization</strong>: Optimizing engagement and helping members discover content they’ll love</li><li><strong>Studio</strong>: Pre and post-production workflows</li><li><strong>Payments</strong><em>: </em>Fraud detection, payment routing, and recurring billing optimization</li><li><strong>Ads</strong>: Our newest domain, requiring real-time decisioning and targeting</li></ul><p>… and a growing number of additional use cases across the company</p><p>Each domain operates with a different tech stack, different business metrics, and a distinct organizational structure. While this diversity is a testament to how machine learning has evolved to drive value across many verticals at Netflix, this growth introduces a new challenge: <strong>enabling cross-pollination of models and data across domains.</strong></p><h3>The Challenge: A Fragmented ML Landscape</h3><p>As our ML investments scaled across these domains, a critical problem emerged: the models produced largely became black boxes. Without any discovery infrastructure, ML practitioners couldn’t easily collaborate or share work across business verticals.</p><p>Consider a concrete example: <a href="https://netflixtechblog.com/mediafm-the-multimodal-ai-foundation-for-media-understanding-at-netflix-e8c28df82e2d">content embeddings</a>. Our Studio teams create sophisticated embeddings that identify scene boundaries, detect visual transitions, and understand content structure. These embeddings were originally built for production workflows.</p><p>But those same embeddings could be incredibly valuable elsewhere. Ads could hypothetically use content embeddings for context matching (ensuring advertisements align with the tone and content of what’s currently playing). Personalization could leverage them for episodic merchandising and recommendations (matching the topic or mood of an episode with a user’s preferred viewing preferences). Yet making this cross-pollination happen is extraordinarily difficult.</p><p>Why? Our ML tools exist in silos, each with its own backend services and user interface. The model registry is unaware of which A/B tests were using its models, and the pipeline orchestrator is unaware of downstream model dependencies. ML practitioners have to traverse multiple systems to answer basic questions about their work. Finding a model requires opening the model registry, understanding its lineage means switching to the pipeline orchestrator, and tracking which A/B tests use that model requires navigating to the experimentation platform. This fragmentation prevents practitioners from answering critical questions:</p><ul><li><strong>Discovery: </strong>What features exist? What data sources are available for generating features for a model?</li><li><strong>Lineage:</strong> Which pipeline is generating data for a specific model? What data sources feed those features?</li><li><strong>Impact:</strong> Which A/B tests are running this model? Which models will break if I change this feature? Who owns each piece of this chain?</li></ul><h3>The Hard Problem: Connecting everything</h3><p>The real challenge wasn’t just building a consolidated UI. We needed to connect the different pieces of infrastructure our ML practitioners were using to perform different parts of the ML lifecycle.</p><p>Our ML ecosystem generates metadata from dozens of sources:</p><ul><li>Pipeline orchestration systems emit execution details, stage dependencies, and data transformations</li><li>Deployed model registry tracks model versions, artifacts, staleness, and deployment history</li><li>Experimentation platform manages A/B tests and their configurations</li><li>Feature store catalog feature definitions and usage</li><li>AI Dataset platform tracks the creation, management, discovery, and loading of datasets.</li><li>Identity platform maintains user, team, and organization metadata</li></ul><p>Each system employs different formats, identifiers, and mental models. The hard technical problem we had to solve was: <strong>How do we collect this heterogeneous metadata, transform it into a unified entity model, and build a connected graph that enables true exploration and collaboration across business domains?</strong></p><h4>The Solution: Metadata Service and the Model Lifecycle Graph</h4><p>Our answer was the Metadata Service (MDS), which builds a Model Lifecycle Graph that indexes and connects ML-related entities across Netflix. MDS is optimized for real-time ingestion of ML metadata (e.g., models, features, pipelines, experiments, datasets) and to answer cross-domain questions such as “Which experiments are running this model?” or “Which models share these features?” It is the foundation that enables discovery, ingesting events from diverse sources, enriching them with context, and materializing relationships across entities.</p><p>Our vision: to make every ML asset at Netflix discoverable, understandable, and reusable by every ML practitioner, regardless of their team or domain.</p><h3>Core Abstractions: The Vocabulary of the System</h3><p>Before diving into the technical implementation, it’s helpful to understand the conceptual model that underpins MDS. This vocabulary enables consistent communication across teams and systems:</p><p><strong>Component:</strong> Any object that is uniquely addressable using an AI Platform’s (AIP) Uniform Resource Identifier (URI). An AIP URI follows the formataip://&lt;componentType&gt;/&lt;platformId&gt;/&lt;resourceId&gt;, ensuring global uniqueness. For example:</p><ul><li>Models: aip://model/registry/ranking-v5</li><li>Users: aip://user/identity/alice</li><li>Pipelines: aip://pipeline/orchestrator/weekly-training</li></ul><p><strong>Entity:</strong> A component within the ML ecosystem, characterized by additional properties such as name, description, creation date, and owners. Entities represent ML-specific assets, such as models, features, and pipelines.</p><p><strong>Entity Type:</strong> A group of entities that share the same data shape. A data shape is a set of property constraints that specify the attributes and relationships an entity must have.</p><p><strong>Domain:</strong> A functional grouping of related entity types that defines the abstract interface for a category of ML assets. For example, the Models domain defines what a Model and Model Instance look like, while the Pipelines domain defines Schedules, Requests, and Executions.</p><p><strong>Provider:</strong> A concrete implementation of a domain, backed by a specific source system. For example, the Models domain is currently backed by our internal model registry. This separation allows MDS to support multiple providers for the same domain. If a new model registry were introduced, it could be added as an additional provider without changing the domain interface.</p><p>We can summarize these concepts with a concrete example:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RQuXyzwooTZcUZ5rCejOug.png" /></figure><p>This URI-based addressing scheme is crucial as it allows any service to reference any ML asset with a single string, and MDS can resolve that reference back to rich, connected metadata.</p><h3><strong>From Events to Entities to Graph</strong></h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dXe2XJMjTpZ6o5wwvVxGUA.png" /></figure><p>The journey from raw system events to a queryable graph happens in stages. Let’s walk through each with a concrete example: connecting a model to its A/B tests through relationship inference.</p><h4>1 Event Ingestion</h4><p>MDS integrates with various source systems via Kafka and AWS SNS/SQS, consuming events in real-time. Source systems emit thin events that include an identifier and an event type.</p><p>Example event:</p><pre>{<br>  &quot;event_type&quot;: &quot;model_instance_created&quot;,<br>  &quot;instance_id&quot;: &quot;ranking-model-v5-20XX0101&quot;,<br>  ...<br>}</pre><p>This design keeps producers simple. Source systems only need to announce that a change occurred, without building complete payloads or understanding downstream requirements.</p><p>Each source system has dedicated event handlers in MDS:</p><ul><li><strong>Pipeline Orchestration</strong>: Ingests pipeline execution events, including node definitions, schedules, requests, and job attempts</li><li><strong>Model Registry</strong>: Captures model deployments, configurations, and version updates</li><li><strong>Feature Store</strong>: Tracks feature definitions and their versions</li><li><strong>Experimentation Platform</strong>: Monitors A/B test configurations and allocations</li><li><strong>Datasets:</strong> Tracks ML datasets and their versions</li><li><strong>Identity Platform</strong>: Maintains ownership and team membership information</li></ul><h4>2 Entity Enrichment</h4><p>MDS implements a hydration contract for each event type. When an event arrives, MDS:</p><ol><li>Validates the event schema</li><li>Calls the source system’s API to fetch the complete, current state</li><li>Transforms the response into a normalized entity</li></ol><p>This design has a crucial property: the order of events doesn’t matter. MDS always fetches the latest facts from the source of truth. This pattern decouples the event stream from state consistency. If the event bus drops a message or delivers it out of order, the next event corrects the state. The event stream becomes a notification of change rather than a log of changes.</p><p>This notification of change pattern has a few important tradeoffs. On the plus side, it keeps producers simple, makes us robust to out-of-order or dropped events, and ensures that MDS can always reconcile to the latest state by reading from the source of truth. The tradeoff is that we place additional read load on source systems during hydration and need to be deliberate about rate limiting, caching, and backoff in our enrichment workers so that we don’t overload them.</p><p>For our ranking model example, when the model_instance_created event arrives, MDS calls the Model Registry API: GET /api/v1/instances/ranking-model-v5-20XX0101</p><p>The registry responds with a full descriptor. Example response (key fields only):</p><pre>{<br>  &quot;id&quot;: &quot;ranking-model-v5-20XX0101&quot;,<br>  &quot;pipeline_run_id&quot;: &quot;train-weekly-ranking-20XX0101&quot;,<br>  &quot;owner_emails&quot;: [&quot;alice@netflix.com&quot;],<br>  &quot;labels&quot;: [{&quot;key&quot;: &quot;team&quot;, &quot;value&quot;: &quot;personalization&quot;}],<br>  ...<br>}</pre><h4>3 Data Transformation and Normalization</h4><p>Raw events are heterogeneous and each source system has its own schema and semantics. MDS workers transform these events into a unified entity model with standardized fields.</p><p>Without normalization, downstream consumers would need to understand every source system’s schema. Normalization creates a consistent interface, allowing queries and relationships to work across all entity types. Here is an example.</p><p>Normalized MDS entity:</p><pre>{<br>  &quot;id&quot;: &quot;aip://model/registry/ranking-model-v5-20XX0101&quot;,<br>  &quot;pipeline_run&quot;: &quot;aip://pipeline-run/orchestrator/train-weekly-ranking-20XX0101&quot;,<br>  &quot;entity_type&quot;: &quot;ModelInstance&quot;,<br>  &quot;owners&quot;: [&quot;aip://user/identity/alice&quot;],<br>  &quot;tags&quot;: [{&quot;tag&quot;: &quot;team&quot;, &quot;value&quot;: &quot;personalization&quot;}],<br>  ...<br>}</pre><p>The normalization process standardizes field names and formats. For example, platform-specific IDs become global AIP URIs, owner_emails becomes owners with resolved user URIs, and labels become tags. Foreign keys like pipeline_run_id are transformed into entity references. However, there’s still no reference to which A/B tests are using this model. The Model Registry doesn’t track experiments, and the Experimentation Platform doesn’t track which pipeline produced a given model. This is where knowledge enrichment becomes critical.</p><h4>4 Storage and Indexing</h4><p>Once normalized, entities are persisted to Datomic and immediately indexed in Elasticsearch. This happens synchronously within the event processing flow.</p><p><strong>Datomic for Caching and Relationships</strong><br>Normalized entities are first written to Datomic, which serves as both a local cache and a graph database.</p><p>Why Datomic? Datomic serves as both the system of record for MDS and the working dataset for enrichment processes. Its immutable fact model means we can continuously add relationships without losing the original entity state.</p><p><strong>What we store:</strong></p><ul><li>All entity attributes as facts</li><li>Entity references (foreign keys that may point to entities not yet fully resolved)</li><li>All relationships as reified edges (added by enrichment processes)</li><li>Entity lifecycle state (tracking which entities are fully enriched vs awaiting hydration)</li></ul><p><strong>This enables:</strong></p><ul><li><strong>Complex graph traversals:</strong> Navigate from a model to its features to their data sources in a single query</li><li><strong>Entity relationships:</strong> Join across multiple domains without N+1 query problems</li><li><strong>Flexible schema evolution:</strong> Easy to add new entity types and attributes as the catalog grows</li><li><strong>Progressive enrichment</strong>: Background jobs efficiently identify and process entities requiring additional hydration, enabling gradual graph completion without reprocessing fully enriched entities</li></ul><p>In practice, we use Datomic for relationship-heavy, navigational queries such as:</p><ul><li>Starting from this model instance, show me all upstream datasets and downstream experiments.</li><li>Given this feature, list all consuming models and their owning teams.</li></ul><p>These queries often span multiple hops in the graph and benefit from Datomic’s immutable fact model and efficient joins across entity relationships.</p><p><strong>Elasticsearch for Discovery</strong><br>Immediately after writing to Datomic, entities are indexed in Elasticsearch to power fast, full-text search across the catalog.</p><p><strong>What we index:</strong></p><ul><li>Primary fields: Entity name, description, entity type, owner names</li><li>Relationship metadata: Names of related entities (e.g., a model’s features, pipelines, A/B tests) stored in the related field</li><li>Tags: Domain-specific metadata stored as key-value pairs (e.g., <em>team::personalization, env::production, model.state::released</em>)</li></ul><p><strong>Index structure:</strong></p><ul><li>Single entities index: All entity types (models, features, pipelines, etc.) are indexed in one unified index, differentiated by the entityType field</li><li>Separate owners index: Dedicated index for users and groups to enable cross-entity owner searches</li><li>Relevance boosting: Exact name matches score higher than other relevant matches</li></ul><p><strong>This enables:</strong></p><ul><li>Multi-field text search across entity names, descriptions, tags, and related metadata</li><li>Relevance ranking with boosting (exact name matches score significantly higher)</li><li>Complex filtering by entity type, ownership, tags, and domain-specific attributes (stored as tags)</li><li>Fuzzy matching to handle typos and partial queries</li></ul><p>Elasticsearch powers the entry point into the system: users typically start with a free-text search in the AIP Portal (for a model name, a team, or a domain term), and then switch to graph navigation once they land on an entity page. Indexing happens in near real-time as part of the ingestion and enrichment workflows, so changes are usually visible in the Portal with a short delay that is acceptable for interactive use.</p><h4>5 Knowledge Enrichment and Graph Formation</h4><p>Once entity metadata is persisted in Datomic, scheduled background processes take over to discover and materialize relationships. These enrichment jobs run periodically, scanning for uncached or partially resolved entities (entities that exist only as references without full metadata).</p><p>The enrichment workflow:</p><ul><li><strong>Identify candidates:</strong> Find entities marked as uncached or with unresolved references</li><li><strong>Hydrate relationships:</strong> Query source-of-truth systems to fetch related entity details</li><li><strong>Materialize edges:</strong> Write discovered relationships back to Datomic</li><li><strong>Re-index:</strong> Trigger Elasticsearch indexing for updated entities</li><li><strong>Mark as enriched:</strong> Update entity status to prevent redundant processing</li></ul><p>This asynchronous approach allows MDS to handle the computational cost of graph formation without blocking real-time event ingestion. It also enables retry logic and gradual enrichment as new entities become available.</p><p>Because enrichment is asynchronous, newly discovered relationships may appear with a short delay after the underlying entities are created (typically minutes rather than seconds). We track when each entity was last enriched and surface this timestamp in the AIP Portal, so practitioners can reason about staleness and know when it’s safe to rely on a particular relationship for debugging or impact analysis.</p><p><strong>Why enrich?</strong> Source systems are purpose-built and don’t know about entities in other domains. Enrichment discovers and materializes cross-system relationships that enable powerful lineage and impact queries.</p><h4>Example: Connecting Models to A/B Tests</h4><p>When MDS processes a new model instance, background enrichment jobs discover relationships through multi-hop inference:</p><p><strong>Step 1: Direct link to pipeline</strong></p><p>The model references a pipeline_run_id. An enrichment job hydrates the pipeline and discovers its A/B test associations: GET /api/v1/pipeline-runs/train-weekly-ranking-20XX0101</p><p>Response:</p><pre>{<br>&quot;run_id&quot;: &quot;train-weekly-ranking-20XX0101&quot;, &quot;pipeline&quot;:  &quot;weekly-ranking-trainer&quot;,<br>&quot;ab_test_cells&quot;: [<br>   {&quot;test_id&quot;: &quot;12345&quot;,&quot;cell_number&quot;: 2,&quot;cell_name&quot;: &quot;treatment_ranking_v5&quot;}<br> ]<br> ...<br>}</pre><p><strong>Step 2: Discover A/B test context</strong><br>The enrichment job discovers the pipeline ran for A/B test cell #2 and queries the Experimentation Platform for test details: GET /api/v1/tests/12345</p><pre>{<br> &quot;test_id&quot;: &quot;12345&quot;,<br> &quot;name&quot;: &quot;Ranking Model v5 vs v4&quot;,<br> &quot;status&quot;: &quot;ACTIVE&quot;,<br> &quot;cells&quot;: [{&quot;cell_number&quot;: 1, &quot;name&quot;: &quot;control_ranking_v4&quot;}],<br> ...<br>}</pre><p><strong>Step 3: Infer transitive relationships</strong><br>The enrichment job now has the complete chain:</p><ul><li>Model Instance was produced by Pipeline Run</li><li>Pipeline Run was executed for A/B Test Cell #2</li><li>The A/B Test Cell #2 belongs to A/B Test “Ranking Model v5 vs v4”</li><li>Model Instance now gets associated with this A/B Test</li></ul><p>The job writes the inferred relationship back to Datomic and triggers re-indexing, and materializes these edges in the graph. MDS doesn’t just store what it’s told; it derives new knowledge by <em>walking</em> the graph in the background.</p><p><strong>Why this matters:</strong> Without MDS, answering “Which A/B tests are using this model?” requires:</p><ol><li>Looking up the model in the Model Registry</li><li>Finding which pipeline produced it</li><li>Checking the Pipeline Orchestrator for A/B test tags</li><li>Querying the Experimentation Platform for test details</li></ol><p>With the model lifecycle graph, it’s a single query:</p><pre>query {<br>  model(id: &quot;aip://model/registry/ranking-model-v5-20XX0101&quot;) {<br>    name<br>    owners { name }<br>    currentInstance {<br>      version<br>      pipeline {<br>        name<br>        owners { name }<br>      }<br>      features {<br>        edges {<br>          node {<br>            name<br>            data { edges { node { name } } }<br>          }<br>        }<br>      }<br>      associatedAbTests {<br>        name<br>        cells { number name }<br>      }<br>    }<br>  }<br>}</pre><p>The reverse query also works: “What models are being tested in experiment 12345?”</p><h3>Enabling Exploration, Not Just Search</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/682/1*j8moOl19CHOfIDRvfnPk5A.png" /></figure><p>With the Model Lifecycle Graph in place, we shift from entity search to entity exploration. Discovery isn’t just about finding a model; It’s about traversing relationships:</p><ul><li>Start with a model, explore its features</li><li>From features, navigate to the core data driving them</li><li>From the data, trace back to the pipelines generating it</li><li>From pipelines, see which teams own and depend on them</li><li>From experiments, understand which models are being tested</li></ul><p>For example, imagine an engineer investigating a degraded engagement metric for a personalization model. They might:</p><ol><li>Start with the model instance powering the affected recommendations in the AIP Portal.</li><li>Inspect the model’s features and follow a suspicious feature to its upstream dataset.</li><li>From the dataset page, see that its pipeline recently had failed runs and identify the owning team.</li><li>Confirm which A/B tests are currently running this model instance to understand which members and surfaces are impacted.</li></ol><p>Before MDS and the Model Lifecycle Graph, this required manual checks across multiple tools (model registry, pipeline orchestrator, experiment platform). Now it’s a contiguous journey in a single interface.</p><p>This graph-based exploration answers questions that were previously impossible:</p><ul><li>Lineage queries: What is the complete lineage of this model, from training data to production experiments?</li><li>Impact analysis: Which models will be affected if I change this feature?</li><li>Usage discovery: Which A/B tests are using this model?</li><li>Dependency mapping: What data sources does my pipeline transitively depend on?</li><li>Deprecation planning: Which entities are no longer being used and can be retired?</li></ul><p>Every entity has deep context: its creation time, ownership, update history, and most importantly, its relationships to other entities.</p><p>The Model Lifecycle Graph is surfaced to practitioners through the AIP Portal, a unified interface that provides full-text search across all entity types, detailed entity pages with navigable relationships, and personalized views for teams and individuals.</p><p>A typical interaction in the AIP Portal looks like:</p><ul><li><strong>Search:</strong> Type a model, feature, dataset, or team name into the single search box backed by Elasticsearch.</li><li><strong>Inspect:</strong> Land on an entity page that shows key metadata (description, owners, domains, tags) alongside a relationships panel.</li><li><strong>Explore:</strong> Click through to related entities (upstream datasets, downstream experiments, and sibling model versions) to navigate the Model Lifecycle Graph without leaving the portal.</li></ul><p>When new entity types are introduced into MDS, the portal automatically provides baseline search, entity pages, and relationship navigation, and we can then layer on domain-specific visualizations (such as model deployment history or dataset version timelines) over time.</p><h3>The Road Ahead: Open Challenges</h3><p>Building the ML lifecycle graph is an ongoing journey. Significant challenges remain, and these represent the future opportunities for us:</p><ul><li><strong>Tool Proliferation:</strong> As new ML tools emerge, we need robust integration patterns that scale. How do we design plugin architectures that make adding new sources seamless? If we don’t keep up with new tools, practitioners will be forced back into fragmented views, and the Model Lifecycle Graph will lose coverage and trust.</li><li><strong>Domain-Specific Visualizations:</strong> Different entity types require distinct visualization experiences. Model pages should display deployment history, A/B test associations, and performance metrics. Feature pages should highlight data lineage and consuming models. Pipeline pages must show execution history, dependencies, and schedules. Dataset pages require versioning timelines and downstream consumers. How do we design a flexible UI framework that allows each entity type to have its own tailored experience while maintaining consistent navigation and interaction patterns across the portal? Without rich, domain-specific experiences, the portal risks becoming a generic catalog rather than a tool that ML practitioners rely on in their daily workflows.</li><li><strong>Metadata Quality:</strong> Today, MDS ensures data consistency through source-of-truth hydration and schema validation at ingestion. Background enrichment jobs continuously infer relationships and materialize entities from source systems. However, challenges remain in ensuring completeness and timeliness at scale. When source systems fail to emit events, when ownership information becomes stale, or when entities lack descriptions and contextual metadata, the graph’s utility degrades. How do we build automated validation and enrichment systems to detect metadata anomalies, suggest missing relationships, and maintain quality benchmarks across millions of entities? Poor or stale metadata erodes practitioner trust: if the graph is incomplete or incorrect, teams will revert to ad hoc knowledge and one-off integrations rather than using MDS as their source of truth.</li><li><strong>Advanced Relationship Inference:</strong> Beyond explicit relationships declared in source systems, how do we infer implicit connections? Can we detect that two models serve similar purposes based on shared features? Can we recommend features based on usage patterns from similar pipelines? We are in the early stages of exploring these ideas. Done well, they would turn MDS from a passive catalog into an active recommendation engine for ML assets, accelerating reuse and reducing duplicate work across domains.</li></ul><h3>Acknowledgments</h3><p>This work represents the collective effort of stunning colleagues across the AI Platform organization: <a href="https://www.linkedin.com/in/emma-carney-6a700b17a/">Emma Carney</a>, <a href="https://www.linkedin.com/in/megan-ren-7b78a81a8/">Megan Ren</a>, <a href="https://www.linkedin.com/in/nadeem-ahmad-80000983/">Nadeem Ahmad</a>, <a href="https://www.linkedin.com/in/poleniuk/">Pat Oleniuk</a>, <a href="https://www.linkedin.com/in/prateekagarwal17/">Prateek Agarwal</a>, <a href="https://www.linkedin.com/in/tikhakobyan/">Tigran Hakobyan</a>, <a href="https://www.linkedin.com/in/yinglao-liu-6b48b6126/">Yinglao Liu</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5cc6d5828bb1" width="1" height="1" alt=""><hr><p><a href="https://medium.com/netflix-techblog/democratizing-machine-learning-at-netflix-building-the-model-lifecycle-graph-5cc6d5828bb1">Democratizing Machine Learning at Netflix: Building the Model Lifecycle Graph</a> was originally published in <a href="https://medium.com/netflix-techblog">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[State of Routing in Model Serving]]></title>
            <link>https://medium.com/netflix-techblog/state-of-routing-in-model-serving-16e22fe18741?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/16e22fe18741</guid>
            <category><![CDATA[ai-platform]]></category>
            <category><![CDATA[distributed-systems]]></category>
            <category><![CDATA[infrastructure]]></category>
            <category><![CDATA[machine-learning]]></category>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Fri, 01 May 2026 21:03:13 GMT</pubDate>
            <atom:updated>2026-05-01T21:22:25.368Z</atom:updated>
            <content:encoded><![CDATA[<p>By <a href="https://www.linkedin.com/in/nipunk/">Nipun Kumar</a>, <a href="https://www.linkedin.com/in/rajatsshah/">Rajat Shah</a>, <a href="https://www.linkedin.com/in/peterchng/">Peter Chng</a></p><h3>Introduction</h3><p><em>This is the first blog post in a multi-part series that shares technical insights into how our ML model serving infrastructure powers several personalized experiences at scale across various domains (e.g., title recommendations, commerce). In this introductory blog post, we will dive into our domain-independent API abstraction and its traffic routing capabilities that the central ML model serving platform exposes to several domain-specific microservices for model inference. This singular API, or entry point, into the ML model serving platform has significantly increased the speed of innovation for iterating on newer versions of existing ML experiences, as well as enabling completely new product experiences with ML.</em></p><p>Machine Learning use cases powering member experiences on Netflix require rapid iteration and evolution in response to new learnings. The success of our ML model serving infrastructure largely depends on enabling researchers to rapidly experiment with new hypotheses and safely, at scale, release their models into production. Equally important is enabling multiple microservices at Netflix to seamlessly get model inference without exposing the complexities of ML model inference. To achieve this in a uniform and scalable manner, we created a centralized ML serving platform. As of 2025, the platform serves hundreds of model types and versions, netting 1 million requests per second. In this post, we’ll zoom in on a core challenge of any large-scale ML serving system: How to route traffic to the right model instance, on the right cluster shard, for the right user and use case, while preserving a simple abstraction for both client services and model researchers.</p><h3>Background</h3><h3>Models at Netflix</h3><p>To properly frame our discussion, let’s first clarify the distinction between model <em>serving</em> and model <em>inference</em>. At Netflix, the definition of an ML model has historically been somewhat unique. While model <em>inference</em> typically focuses only on an infer(features) -&gt; score capability, models at Netflix act as self-contained workflows that transform inputs to outputs. A “model” encapsulates pre- and post-processing, feature computation logic, and an optional ML-trained component, all packaged in a standard format suitable for use across multiple contexts. We refer to the end-to-end execution of this workflow as model <em>serving</em>. This distinction matters because our routing and API abstractions operate at the level of workflows, not just individual scoring functions.</p><p>A few <em>simplified</em> examples of model serving use cases:</p><p><strong>Use case</strong>: Personalized Continue Watching row on Netflix Homepage</p><ul><li>Input: UserId, Country, Device ID</li><li>Output: Ranked List of movies and shows (aka title): [titleId1, titleId2, titleId3,…]</li></ul><p><strong>Use case</strong>: Payment Fraud Detection</p><ul><li>Input: UserId, Country, Payment Transaction details</li><li>Output: Probability of the transaction being fraudulent</li></ul><p>A typical flow of this serving workflow is depicted below:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/804/1*V0IaBLQSyADbjdnlhRDJdA.png" /></figure><p>To achieve this higher level of abstraction, the model definition contains a list of facts (raw, unprocessed data or observations built as states in different business workflows) that it needs to compute features, and it relies on the model serving platform to supply these facts at serving time by calling several other microservices. Likewise, during offline training, <a href="https://netflixtechblog.com/evolution-of-ml-fact-store-5941d3231762">Netflix’s ML fact store</a> provides snapshots for bulk access to facilitate feature computation.</p><p>The important takeaway from this model definition is that the calling services only need to provide standard request context (such as userId, country, device), and the relevant domain context (such as titles to rank, or payment transaction for fraud detection), and the model can itself compute features and perform inference as part of the execution flow. This common set of request contexts across domains enables them to share a standard API abstraction and standardizes how various client microservices can uniformly integrate with the serving app. Furthermore, clients are shielded from the model selection and execution, allowing the model architecture and data inputs to evolve with minimal client coordination.</p><p>This post focuses on showcasing the technical details to support this design paradigm. We’ll first describe how we implemented this abstraction with Switchboard, a centralized routing service, and then discuss the operational challenges we encountered at scale and how they led us to the Lightbulb architecture.</p><h3>ML Model Serving Platform Principles</h3><p>We envisioned a central model serving platform for all of Netflix’s member-facing ML Model serving needs. This ambitious effort required principled thinking to provide the right level of abstraction for both the researchers and client applications. The following ideas, which are relevant to the topic of this blog post, ensured that the platform acts as an enabler of rapid ML innovation and limits the exposure of ML model iterations to the client apps:</p><ul><li><strong>Model innovation independent of client apps: </strong>There should be only a one-time integration effort by the calling app with the ML serving platform for a new use case. After that, almost all model iterations, including intermediate model A/B experiments, should be mostly opaque to the calling apps. This implies that the platform should handle tasks such as model selection based on a user’s A/B allocation, fetching additional data needed by experimental models, logging for further training or observability, and more. This also benefits the ML researcher, as they only need to coordinate with one platform for model innovation.</li><li><strong>Decouple clients from model sharding: </strong>Models are distributed across multiple serving compute cluster shards, each with its own Virtual IP (VIP) Address. Various factors, such as traffic patterns, SLAs, model architecture, and CPU/Memory availability, affect model-to-cluster mapping, and changes to this mapping result in changes to the VIP address at which a model is reachable. The serving platform should make clients agnostic to such frequent VIP address changes while ensuring high availability.</li><li><strong>Flexible traffic routing rules: </strong>Support flexible mechanisms to introduce new traffic routing rules. This includes supporting traffic routing based on A/B experiments, providing a knob to slowly shift traffic to new models and VIP addresses, and allowing client overrides.</li></ul><h3>Introducing Switchboard</h3><p>Standard out-of-the-box API Gateway solutions (such as AWS API Gateway, a standalone Service Mesh proxy) did not meet all our requirements. In particular, we needed first-class integration with Netflix’s experimentation platform, the ability to expose gRPC endpoints to clients, and the ability to use rich domain-specific context for routing customizations, which generic proxies were not designed to handle. Furthermore, the platform required customizations to model-specific lifecycle stages (shadow mode, canaries, rollbacks) to enable safe rollouts and migrations.</p><p>Hence, we embarked on building a custom service that serves as a flexible proxy layer for all traffic, handling over 1 million requests per second while maintaining high availability and reliability. We named it Switchboard.</p><p>Switchboard serves as the central entry point for the system, <strong>acting as a mandatory interface </strong>for all clients to access the appropriate model based on their context. Its role is to perform context-aware routing and to apply any configured context enrichment to the model inputs.</p><p>Here is a visual representation of the request flow from different clients to different serving clusters:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/935/1*HEV6B_6F5ci3dyoKXyq55A.png" /></figure><h3>Objective Abstraction</h3><p>To support this system design, we introduce the concept of an “Objective”. It’s an Enumeration defined by the serving platform that every request into the system must provide. It has three key purposes:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/703/1*V6bhyHhYQ4W5baVzvygW9g.png" /></figure><p>In short, an <strong>Objective</strong> is the serving platform’s name for a specific business use case (e.g., ContinueWatchingRanking), which decouples clients from concrete models and guides the platform’s routing and model selection decisions.</p><h3>Key Capabilities of Switchboard</h3><p>To summarize, these are the key capabilities of Switchboard:</p><ol><li><strong>Common Client Abstraction: </strong>Switchboard provides a single point of contact for all our clients’ model needs. When clients wish to consume additional models for new ML applications addressing the same business need, there is no new service dependency to introduce or new clients to manage to make requests to the models. From an ML Ops perspective, this also gives us knobs to control client rate limits across model versions and manage central concurrency limits to deal with bad clients.</li><li><strong>Context-Aware Routing:</strong> Switchboard can route a request based on a rich set of contextual features, such as the user’s current device, locale, ranking surface type (e.g., home page vs. search results), or the current A/B test a user is in.</li><li><strong>Dynamic Traffic Splitting:</strong> It enables real-time traffic splitting for canary deployments and experimentation. This allows engineers to safely roll out a new model version to a small, controlled percentage of users before a full launch.</li><li><strong>Model Versioning and Lifecycle Management:</strong> Switchboard inherently manages concurrent request traffic to multiple versions of the same model. This is crucial for:</li></ol><ul><li><strong>Shadow Mode Testing:</strong> Routing production traffic to a new model version without affecting the user experience, enabling performance comparisons.</li><li><strong>Instant Rollback:</strong> Immediate switching of traffic away from a problematic new model version back to a stable one.</li></ul><p>But is this the whole story? Not quite. Introducing this routing layer adds complexity to our model deployment cycles. In addition, we need a mechanism to collect the context-based routing information from the researchers when they choose to deploy model variants.</p><h3>The Glue — Switchboard Rules</h3><p>Given that Objectives serve as the contract between clients and the serving platform, we needed a way for researchers to attach model variants, experiments, and traffic splits to those Objectives without changing client code. This is where Switchboard Rules comes in.</p><p>The primary UX for model researchers to define models associated with an objective in a flexible manner is a JavaScript configuration, which we call <em>Switchboard Rules</em>. It’s used to produce a set of rules (typically a JSON file) that primarily dictate the following things to the serving platform:</p><ol><li>The default model to use for a given Objective</li><li>A/B experiments to configure for a set of Objectives and the corresponding models to load for those experiments</li><li>Customizations to gradually shift traffic to a new model</li></ol><p>Here is an example of an A/B test rule in the context of the Continue Watching row:</p><pre>/**<br>Configuration rule written by a Model Researcher to add an A/B experiment in the Model Serving system.<br>Cell 1: Uses the default, currently productized model<br>Cell 2 and Cell 3: Use different experimental (candidate) models<br>**/<br><br>function defineAB12345Rule() {<br>    const abTestId = 12345;<br><br>    const objectives = Objectives.ContinueWatchingRanking;<br>    const abTestCellToModel = {<br>        1: {name: &quot;netflix-continue-watching-model-default&quot;},<br>        2: {name: &quot;netflix-continue-watching-model-cell-2&quot;},<br>        3: {name: &quot;netflix-continue-watching-model-cell-3&quot;}<br>    };<br><br>    return {<br>        cellToModel: abTestCellToModel,<br>        abTestId: abTestId,<br>        targetObjectives: [objectives],<br>        modelInputType: constants.TITLE_INPUT_TYPE,<br>        modelType: &#39;SCORER&#39;<br>    };<br>}</pre><p>These rules are consumed by both the Switchboard and the Model Serving clusters. Given these rules, the serving platform components can take various actions, some detailed below:</p><p><strong>Control Plane Flow</strong>:</p><ol><li><strong>Assignment:</strong> Produce model-to-cluster shard assignment.</li><li><strong>Validation:</strong> Load all specified models into the Serving Cluster Shard and validate model dependencies to ensure successful execution.</li><li><strong>Mapping:</strong> Provide the model-to-shard VIP address mapping to Switchboard.</li></ol><p><strong>Data Plane Flow</strong>:</p><ol><li><strong>Allocation:</strong> If the request is for Objective=ContinueWatchingRanking, query the <a href="https://netflixtechblog.com/its-all-a-bout-testing-the-netflix-experimentation-platform-4e1ca458c15">Experimentation Platform</a> for the userId’s cell allocation.</li><li><strong>Model Selection:</strong> Use the allocation and A/B test rule to select the appropriate model.</li><li><strong>Request Routing:</strong> Route the request to the serving cluster shard with the selected model and context.</li><li><strong>Model Execution (on the serving host):</strong> Run the model workflow steps and return the response.</li></ol><p>A key highlight of this setup is the decoupling of the experimentation config from the serving platform code. This includes having an independent release cycle for the rules, separate from the code deployments. <a href="https://netflixtechblog.com/how-netflix-microservices-tackle-dataset-pub-sub-4a068adcc9a">Netflix’s Gutenberg</a> system provides an excellent ecosystem that enables a flexible pub-sub architecture, facilitating proper versioning, dynamic loading, easy rollbacks, and more. Both Switchboard and the Serving Cluster Host subscribe to the same Switchboard Rules configuration.</p><p>To prevent race conditions and ensure proper sync of the dynamic Switchboard Rules configuration, the following flow is considered:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/944/1*iSNuD8ZuSC9E2zN-wZdH-A.png" /></figure><h3>Evolving Challenges</h3><p>Switchboard solved the primary problem of improving model iteration and innovation velocity, and provided an excellent ML serving abstraction to over 30 service clients. However, as the system scale increased, a few challenges and problems with this design became apparent:</p><ul><li><strong>Single point of failure: </strong>The presence of Switchboard in the critical request path clearly highlights the risks of shutting down access to all serving hosts in extreme cases, such as unintentional bugs or noisy neighbors sending excessive traffic.</li><li><em>Why this matters: Switchboard became a shared dependency whose failure would degrade or disable multiple ML-powered experiences at Netflix.</em></li><li><strong>Added latency due to additional network hop:</strong> Switchboard in the request path adds between 10–20ms of latency due to serialization-deserialization operations, depending on payload size. Additionally, it further exposes a request to tail latency amplification.</li><li><em>Why this matters: The added latency is unacceptable for some latency-sensitive clients, resulting in end-user impact due to service timeouts.</em></li><li><strong>Reduced Client flexibility</strong>: Switchboard obscures visibility into client request origins from the serving clusters. Consequently, distinguishing data logged for real vs artificial traffic, which is essential for model training, is difficult and requires ongoing customization and increased MLOps overhead.</li><li><em>Why this matters: It makes it harder to do tenant separation and test traffic isolation.</em></li></ul><h3>What Next? — Lightbulb</h3><p>The aforementioned challenges of operating Switchboard at scale forced us to rethink the core implementation while retaining its key features. Our goal was not to throw away Switchboard’s design, but to refactor where and how its responsibilities were executed, keeping the benefits while reducing risk and latency. Particularly:</p><ul><li><em>Common Client Abstraction</em></li><li><em>Decouple clients from model sharding</em></li><li><em>Flexible traffic routing rules</em></li><li><em>Lightweight system client</em></li><li><em>Single place to define model and experimentation config</em></li><li><em>Fast experimentation config propagation</em></li><li><em>Fallback and client-side caching in case of failures</em></li></ul><p>However, we did want to address some of the previous design choices to move forward with:</p><ul><li><strong>Remove the routing service from the direct request path: </strong>Having a single service in the active request path introduces another failure mode and limits fallback flexibility. While routing rules change infrequently, maintaining consistency comes at the cost of increased availability risks.</li><li><strong>Separate model inputs from the request metadata</strong>: In certain cases, the request payload could be quite large. Needing to deserialize and then re-serialize the payload as it flowed through Switchboard to make a routing decision was a significant contributor to latency and increased serving costs.</li><li><strong>Provide better isolation for the routing layer: </strong>Consolidating multiple use cases (tenants) into a single routing cluster poses two main challenges. First, error propagation posed a risk, as a surge of problematic requests from one tenant could cascade errors back to Switchboard, potentially impacting other users. Second, the cluster had to accommodate diverse latency requirements because the requests from different use cases varied significantly in complexity.</li></ul><p>This required some changes in our setup flow: While it largely remained unchanged, however, we created separate components for Routing and Model Selection (Lightbulb):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/897/1*wtVpe5xJMNEENitvkd1FPQ.png" /></figure><p>We now take the rules for an Objective and break them into distinct sets of configuration:</p><ul><li><strong>Model Serving Configuration</strong>: This allows us to determine which model should be used at request time, along with the required metadata</li><li><strong>Routing Rules</strong>: Given a model we want to serve at request time, this tells us which VIP the request should be routed to.</li></ul><p>The Data Plane changes also reflect this separation, as we now rely on <a href="https://github.com/envoyproxy/envoy">Envoy</a> to take care of the routing details:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/912/1*jbW4NlcnKucGKBi_vNjXbw.png" /></figure><p>Envoy is <a href="https://netflixtechblog.com/zero-configuration-service-mesh-with-on-demand-cluster-discovery-ac6483b52a51">already used</a> for all egress communication between apps at Netflix, and it can route requests to different clusters (VIPs) based on the configurable Routing Rules published from our control plane. However, it lacks the information needed to make routing decisions and the ability to enrich the request body with additional serving parameters required for A/B testing model variants. We introduced Lightbulb to cover this gap:</p><ul><li>Lightbulb consumes the minimal request context, which contains use-case information, and provides the metadata mapping required for routing at the Envoy layer.</li><li>Lightbulb resolves the request context to determine a routingKey configuration along with the <strong>ObjectiveConfig</strong> — this is where we place the model id along with other request-specific configurations required for model execution. This is done to separate the config resolution associated with the request from the placement and routing information needed to reach it on the inference cluster.</li><li>While the routingKey is added to the headers for Envoy proxy to consume, the client adds the ObjectiveConfig parameters to the request itself. This is done to avoid bloating the request headers while passing additional parameters for the model to process the request appropriately.</li><li>The routing of the actual request is performed by the Envoy proxy, which has the metadata to map the routingKey to the actual cluster VIP running the model. Because the routingKey is in a header, this determination can be made with minimal overhead.</li></ul><p>These changes retain the advantages of Switchboard, such as a single integration point, abstraction of model id from use case, context-aware routing, while addressing the challenges we observed over time.</p><h3>Conclusion</h3><p>The evolution from Switchboard to Lightbulb marks a significant architectural refinement in our ML model serving infrastructure. While Switchboard provided the initial abstraction layer critical for rapid innovation, its latency and single-point-of-failure risk posed scaling hurdles. The subsequent adoption of Lightbulb, a decoupled service focused solely on routing metadata, and its integration with Envoy successfully resolved these challenges. This sophisticated new architecture preserves the key benefits — seamless client integration and flexible experimentation — while ensuring reliable, efficient, and scalable delivery of personalized member experiences, positioning us well for future ML growth.</p><p>In future posts in this series, we’ll dive deeper into other aspects of our ML serving platform, including inference and feature fetching, and how they interact with the routing architecture described here.</p><p>Special thanks to <strong>Sura Elamurugu</strong>, <strong>Sri Krishna Vempati</strong>, <strong>Ed Maddox</strong>, and <strong>Sreepathi Prasanna</strong> for their invaluable feedback and partnership in iterating on this idea and bringing this blog post to life.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=16e22fe18741" width="1" height="1" alt=""><hr><p><a href="https://medium.com/netflix-techblog/state-of-routing-in-model-serving-16e22fe18741">State of Routing in Model Serving</a> was originally published in <a href="https://medium.com/netflix-techblog">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[Scaling Camera File Processing at Netflix]]></title>
            <link>https://medium.com/netflix-techblog/scaling-camera-file-processing-at-netflix-6dab2b1e80be?source=rss-c3aeaf49d8a4------2</link>
            <guid isPermaLink="false">https://medium.com/p/6dab2b1e80be</guid>
            <dc:creator><![CDATA[Netflix Technology Blog]]></dc:creator>
            <pubDate>Fri, 24 Apr 2026 15:06:01 GMT</pubDate>
            <atom:updated>2026-04-24T15:06:01.452Z</atom:updated>
            <content:encoded><![CDATA[<p><em>Orchestrating Media Workflows Through Strategic Collaboration</em></p><p>Authors: <a href="https://www.linkedin.com/in/ericreinecke/">Eric Reinecke</a>, <a href="https://www.linkedin.com/in/bhanusrikanth/">Bhanu Srikanth</a></p><h3>Introduction to Content Hub’s Media Production Suite</h3><p>At Netflix, we want to provide filmmakers with the tools they need to produce content at a global scale, with quick turnaround and choice from an extraordinary variety of cameras, formats, workflows, and collaborators. Every series or film arrives with its own creative ambitions and technical requirements. To reduce friction and keep productions moving smoothly, we built <a href="https://netflixtechblog.com/globalizing-productions-with-netflixs-media-production-suite-fc3c108c0a22">Netflix’s Media Production Suite (MPS)</a> with the goal of automating repeatable tasks, standardizing key workflows, and giving productions more time to focus on creative collaboration and craftsmanship.</p><p>A critical part of this effort is how we handle image processing and camera metadata across the hundreds of hours and terabytes of camera footage that Netflix productions ingest on a daily basis. Rather than build every component from scratch, we chose to partner where it made sense–especially in areas where the industry already had trusted, battle-tested solutions.</p><p>This article explores how Netflix’s Media Production Suite integrates with FilmLight’s API (FLAPI) as the core studio media processing engine in Netflix’s cloud compute infrastructure, and how that collaboration helps us deliver smarter, more reliable workflows at scale.</p><h3>Why We Built MPS</h3><p>As Netflix’s production slate grew, so did the complexity of file-based workflows. We saw recurring challenges across productions:</p><ul><li>File wrangling sapping time from creative decision-making</li><li>Inconsistent media handling across shows, regions, or vendors</li><li>Difficult to audit manual processes that are prone to human error</li><li>Duplication of effort as teams reinvented similar workflows for each production</li></ul><p>Content Hub Media Production Suite was created to address these pain points. MPS is designed to:</p><ul><li>Bring efficiency, consistency, and quality control to global productions</li><li>Streamline media management and movement from production through post-production</li><li>Reduce time spent on non-creative file management</li><li>Minimize human error while maximizing creative time</li></ul><p>To achieve this, MPS needed a robust, flexible, and trusted way to handle camera-original media and metadata at scale.</p><h3>The Right Tool for the Job</h3><p>From the start, we knew that building a world-class image processing engine in-house is a significant, long-term commitment: one that would require deep, continuous collaboration with camera manufacturers and the wider industry.</p><p>When designing the system, we set out some core requirements:</p><ul><li><strong>Inspect, trim, and transcode original camera files and metadata</strong> for any Netflix production with trusted color science</li><li><strong>Support a wide variety of cameras and recording formats</strong> used worldwide while staying current as new ones are released</li><li><strong>Run well in our paved-path encoding infrastructure,</strong> enabling us to take advantage of proven compute and storage scalability with robust observability</li></ul><p>FilmLight develops Baselight and Daylight, which are commonly used in the industry for color grading, dailies, and transcoding. Their FilmLight API (FLAPI) allows us to use that same media processing engine as a backend API.</p><p>Rather than duplicating that work, we chose to integrate. FilmLight became a trusted technology partner, and FLAPI is now a foundational part of how MPS processes media.</p><h3>The Media Processing Engine</h3><p>MPS is not a single application; it’s an ecosystem of tools and services that support Netflix productions globally. Within that ecosystem, the FilmLight API plays the following key roles.</p><ol><li>Parsing camera metadata on ingest</li></ol><p>Productions upload media to Netflix’s <strong>Content Hub</strong> with <a href="https://theasc.com/society/ascmitc/asc-media-hash-list">ASC MHL</a> (Media Hash List) files to ensure completeness and integrity of initial ingest, but soon after, it’s important to understand the technical characteristics of each piece of media. We call this workflow phase “inspection.”</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OYBDXSUJ6D0tVXVGO3w5TQ.jpeg" /><figcaption>Footage ingested with MPS is inspected using FLAPI and all metadata is indexed and stored</figcaption></figure><p>At this stage, we:</p><ul><li>Use FLAPI to gather <strong>camera metadata</strong> from the original camera files</li><li>Conform the workflow critical fields to <strong>Netflix’s normalized schema</strong></li><li>Make it <strong>searchable and reusable</strong> for downstream processes</li></ul><p>This metadata is integral to:</p><ul><li>Matching footage based on timing and reel name for automated retrieval</li><li>Debugging (e.g., why a shot looks a certain way after processing)</li><li>Validations and checks across the pipeline</li></ul><p>FLAPI provides consistent, camera-aware insight into footage that may have originated anywhere in the world. Additionally, since we’re able to package FLAPI in a Docker image, we can deploy almost identical code to both cloud and our production compute and storage centers around the world, ensuring a consistent assessment of footage wherever it may exist.</p><p>2. Generating VFX plates and other deliverables</p><p>Visual effects workflows constantly push image processing pipelines to their absolute limits. For MPS to succeed, it must generate images with <strong>accurate</strong> framing, <strong>consistent</strong> color management, and <strong>correct</strong> debayering/decoding parameters — all while maintaining rapid turnaround times.</p><p>To achieve this, we leverage Netflix’s <a href="https://netflixtechblog.com/the-netflix-cosmos-platform-35c14d9351ad">Cosmos</a> compute and storage platform and use open standards to provide predictable and consistent creative control.</p><p>At this phase, we use the FilmLight API to:</p><ul><li><strong>Debayer</strong> original camera files with the correct format-specific decoding parameters</li><li>Crop and de-squeeze images using <strong>Framing Decision Lists (ASC FDL)</strong> to ensure spatial creative decisions are preserved</li><li><strong>Apply ACES Metadata Files (AMF), </strong>providing repeatable color pipelines from dailies through finishing</li><li>Generate <strong>an array of media deliverables</strong> in varied formats</li></ul><p>These processes are automated, repeatable, and auditable. We deliver AMFs alongside the OpenEXRs to ensure recipients know exactly what color transforms are already applied, and which need to be applied to match dailies.</p><p>Because we use FilmLight’s tools on the backend, our workflow specialists can use Baselight on their workstations to manually validate pipeline decisions for productions before the first day of principal photography.</p><h3>The Media Processing Factory in the Cloud</h3><p>Finding an engine that competently processes media in line with open standards is an important part of the equation. To maximize impact, we want to make these tools available to all of the filmmakers we work with. Luckily, we’re no strangers to scaled processing at Netflix, and our <a href="https://netflixtechblog.com/the-netflix-cosmos-platform-35c14d9351ad">Cosmos compute platform</a> was ready for the job!</p><h4>Cloud-first integration</h4><p>The traditional model for this kind of processing in filmmaking has been to invest in beefy computers with large GPUs and high-performance storage arrays to rip through debayering and encoding at breakneck speed. However, constraints in the cloud environment are different.</p><p>Factors that are essential for tools in our runtime environment include that they:</p><ul><li>Are <strong>packageable as Serverless Functions in Linux Docker images</strong> that can be quickly invoked to run a single unit of work and shut down on completion</li><li>Can <strong>run on CPU-only instances</strong> to allow us to take advantage of a wide array of available compute</li><li>Support <strong>headless invocation </strong>via Java, Python, or CLI</li><li><strong>Operate statelessly,</strong> so when things do go wrong, we can simply terminate and re-launch the worker</li></ul><p>Operating within these constraints lets us focus on increasing throughput via parallel encoding rather than focusing on single-instance processing power. We can then target the sweet spot of the cost/performance efficiency curve while still hitting our target turnaround times.</p><p>When tools are API-driven, easily packaged in Linux containers, and don’t require a lot of external state management, Netflix can quickly integrate and deploy them with operational reliability. FilmLight API fit the bill for us. At Netflix, we leverage:</p><ul><li><strong>Java</strong> and <strong>Python</strong> as the primary integration languages</li><li><strong>Ubuntu-based Docker images</strong> with Java and Python code to expose functionality to our workflows</li><li><strong>CPU instances in the cloud and local compute centers</strong> for running inspection, rendering, and trimming jobs</li></ul><p>While FLAPI also supports GPU rendering, CPU instances give us access to a much wider segment of Netflix’s vast encoding compute pool and free up GPU instances for other workloads.</p><p>To use FilmLight API, we bundle it in a package that can be easily installed via a Dockerfile. Then, we built Cosmos Stratum Functions that accept an input clip, output location, and varying parameters such as frame ranges and AMF or FDL files when debayering footage. These functions can be quickly invoked to process a single clip or sub-segment of a clip and shut down again to free up resources.</p><h4>Elastic scaling for production workloads</h4><p>Production workloads are inherently spiky:</p><ul><li>A quiet day on set may mean minimal new footage to inspect.</li><li>A full VFX turnover or pulling trimmed OCF for finishing might require <strong>thousands of parallel renders</strong> in a short time window.</li></ul><p>By deploying FLAPI in the cloud as functions, MPS can:</p><ul><li>Allocate compute on demand and release it when our work queue dies down</li><li>Avoid tying capacity to a fixed pool of local hardware</li><li>Smooth demand across many types of encoding workload in a shared resource pool</li></ul><p>This elasticity lets us swarm pull requests to get them through quickly, then immediately yield resources back to lower priority workloads. Even in peak production periods, we avoid the pain of manually managing render queues and prioritization by avoiding fixed resource allocation. All this means <strong>lightning-fast</strong> turnaround times and <strong>less anxiety</strong> around deadlines for our filmmakers.</p><h3>Designed for Seasoned Pros and Emerging Filmmakers</h3><p>Netflix productions range from highly experienced teams with very specific workflows to newer teams who may be less familiar with potential pitfalls in complex file-based pipelines.</p><p>MPS is designed to support both:</p><ul><li>Industry veterans who need to configure precise, bespoke workflows and trust that underlying image processing will respect those decisions.</li><li>Productions without a color scientist on staff — those who benefit from guardrails and sane defaults that help them avoid common workflow issues (e.g., mismatched color transforms, inconsistent debayering, or incomplete metadata handling).</li></ul><p>The partnership with FilmLight lets Netflix focus on workflow design, orchestration, and production support, while FilmLight focuses on providing competent handling of a wide variety of camera formats with world-class image science!</p><h3>Collaboration and Co-Evolution</h3><p>Netflix aimed to integrate MPS into a wider tool ecosystem by developing a comprehensive solution based on emerging open standards, rather than making MPS a self-contained system. Integrating FLAPI into our system requires more than an API reference–it requires ongoing partnership. FilmLight worked closely with Netflix teams to:</p><ul><li>Align on <strong>feature roadmaps</strong>, particularly around new camera formats and open standards</li><li>Validate the <strong>accuracy and performance</strong> of key operations</li><li>Debug <strong>edge cases</strong> discovered in large-scale, real-world workloads</li><li><strong>Evolve the API</strong> in ways that serve both Netflix and the wider industry</li><li>Create <strong>a positive feedback cycle with open standards</strong> like ACES and ASC FDL to solve for gaps when the rubber hits the road</li></ul><p>One example of this has been with the implementation of <a href="https://draftdocs.acescentral.com/background/about-aces-2/">ACES 2</a>. FilmLight’s developers quickly provided a roadmap for support. As our engineering teams collaborated on integration, we also provided feedback to the ACES technical leadership to quickly address integration challenges and test drive updates in our pipeline.</p><p>This collaborative relationship–built on open communication, joint validation, and feedback to the greater industry–is how we routinely work with FilmLight to ensure we’re not just building something that works for our shows, but also driving a healthy tooling and standards ecosystem.</p><h3>Impact</h3><p>While much of this work takes place behind the scenes, its impact is felt directly by our productions. Our goal with building MPS is for producers, post supervisors, and vendors to experience:</p><ul><li>Fewer delays caused by missing, incomplete, or incorrect media</li><li>Faster turnaround on VFX plates and other technical deliverables</li><li>More predictable, consistent handoffs between editorial, color, and VFX</li><li>Less time spent troubleshooting technical issues, and more time focused on creative review</li></ul><p>In practice, this often shows up as the absence of crisis: the time a VFX vendor doesn’t have to request a re-delivery, or the time editorial doesn’t have to wait for corrected plates, or the time the color facility doesn’t have to reinvent a tone-mapping path because the AMF and ACES pipeline are already in place.</p><h3>Looking Ahead</h3><p>As camera technology, codecs, open standards, and production workflows continue to evolve, so will MPS. The guiding principles remain:</p><ul><li>Automate what’s repeatable</li><li>Centralize what benefits from standardization</li><li>Partner where deep domain expertise already exists</li></ul><p>The integration with FilmLight API is one example of this philosophy in action. By treating image processing as a specialized discipline and collaborating with a trusted industry partner, Netflix is delivering smarter, more reliable workflows to productions worldwide.</p><p>At its core, this partnership supports a simple goal: reduce manual workflow and tool management, giving filmmakers more time to tell stories.</p><h3>Acknowledgements</h3><p>This project is the result of collaboration and iteration over many years. In addition to the authors, the following people have contributed to this work:</p><ul><li>Matthew Donato</li><li>Prabh Nallani</li><li>Andy Schuler</li><li>Jesse Korosi</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6dab2b1e80be" width="1" height="1" alt=""><hr><p><a href="https://medium.com/netflix-techblog/scaling-camera-file-processing-at-netflix-6dab2b1e80be">Scaling Camera File Processing at Netflix</a> was originally published in <a href="https://medium.com/netflix-techblog">Netflix TechBlog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>