Netflix logo

How Netflix queries a billion-edge graph in under 100 milliseconds

Turning a constantly growing graph into answers that feel instant, no matter how the question is shaped

Building the graph is only half the problem. Netflix's Real-Time Distributed Graph (RDG) ingests and stores billions of nodes and edges - but none of that matters if nobody can actually ask it a question and get an answer back fast. This is where Netflix's data engineering team faced a different kind of challenge: some questions touch a handful of records, others fan out across hundreds of relationships, and some need to chain several hops deep. All of them needed to come back in well under a second, most in well under 100 milliseconds, on a graph with roughly 8 billion nodes and 150 billion edges.

Terms worth knowing before you read on

Fan-out

How many results a single step of a query can spread into - asking for one account's devices might fan out into hundreds of device records at once.

Breadth-first traversal

Exploring a graph one full layer at a time (all profiles, then all their content) instead of following one path all the way to the end before trying the next.

P50 / P99 latency

P50 is the typical response time (half of requests are faster). P99 is the response time for the slowest 1% of requests - the number that tells you how bad the worst case gets.

Interactive

Walk the pipeline

Step through each stage of how this actually works, in order.

Stage 1 of 5 · gRPC request

Client sends a traversal request; the engine parses it into a concrete execution plan of hops, limits, and filters.

The problem: 'querying the graph' means very different things depending on who's asking

Netflix's team found that graph queries pull the system in two very different directions. A 'shallow and wide' query - something like 'which devices has this account streamed from in the last 30 days' - is only one hop away from the starting point, but that one hop can fan out into hundreds of edges to fetch, filter, and aggregate.

A 'deep and narrow' query is the opposite problem: something like 'across every profile on this account, show me the full Stranger Things viewing history' has to walk multiple hops in sequence - first find the profiles, then find what each profile watched. If each hop takes even 10 milliseconds of network time, four sequential hops alone burn 40ms before any real work happens.

Supporting both kinds of queries, at tens of thousands of queries per second, on a graph that never stops growing, is what shaped every design decision that follows.

Why breadth-first beats depth-first when every hop is a network call

The intuitive way to walk a graph is depth-first: pick a path, follow it all the way, backtrack, try the next one. Netflix's team explicitly avoided this, because in a distributed system, each hop means a network round trip, and depth-first would trace one profile's entire history before even starting on the next profile - wasting the chance to batch those lookups together.

Instead, the engine works breadth-first: fetch all profiles for an account in one round trip, then fetch the relevant edges for all of those profiles together in a second round trip, and so on. A query that chains two hops becomes two rounds of parallel work instead of one long sequential chain - which is a big part of how a multi-hop query stays under 100ms.

Why the whole engine is built async-first, not thread-per-request

Most of the delay in answering a graph query isn't computation - it's waiting: waiting on the storage layer, waiting on a cache, waiting on an enrichment service. A traditional design would assign one thread per in-flight query, and with thousands of concurrent queries, most of those threads would just sit idle waiting for a network response to come back.

Netflix built the entire query engine around asynchronous composition instead. A small pool of 16 to 24 threads handles thousands of concurrent requests, because no single thread ever blocks waiting on I/O - while one storage call is in flight, that thread picks up other work and comes back for the result later. The team describes this as the foundational decision everything else in the system rests on.

Parallel work, kept from turning into chaos

Breadth-first traversal only helps if the work within each level actually runs in parallel. Netflix's team compares their design to a professional kitchen: separate stations for separate kinds of work - one pool of workers for fetching nodes, another for reading adjacency lists, another for enrichment calls - so a slowdown in one station doesn't stall the others.

On top of that, the system uses adaptive concurrency limiting: when things are healthy, it slowly raises how much work it allows in flight at once; when errors or timeouts start climbing, it backs off by a much larger step. That combination is what lets the engine run many lookups in parallel without accidentally overwhelming the storage layer underneath it.

Filtering as close to the data as possible

A profile might have hundreds of viewing events on record, but a query usually only cares about a slice of that - the last 30 days, say. Rather than pulling everything back and trimming it afterward, Netflix streams adjacency data in small batches and applies filters as each batch arrives, stopping as soon as it has enough to satisfy the query.

The team also built a layered system of defaults and overrides - global defaults, per-query overrides, and per-edge-type limits - so different teams can ask for different lookback windows or edge limits without needing a code change every time. They describe this as having eliminated an entire category of one-off feature requests.

Caching what's actually worth remembering

Not everything in the graph changes at the same speed. An account's plan type or a show's metadata barely changes; who-watched-what-and-when changes constantly. Netflix caches the slow-changing, frequently-accessed data - accounts, profiles, content - in a distributed cache (EVCache), which brought hit rates up to 70-80% on those lookups.

The subtler decision was knowing what not to cache: a node that's about to fall outside the graph's retention window isn't worth caching even briefly, so the system weighs a node's last-activity time against the retention period before deciding whether caching it is worthwhile at all.

Takeaway

The serving layer's speed doesn't come from one trick - it's breadth-first traversal turning sequential hops into parallel rounds, an async-first design that lets a handful of threads carry thousands of concurrent queries, deliberately bounded parallelism instead of unbounded concurrency, and caching that's selective rather than aggressive. The transferable lesson: filter as early as possible, parallelize on purpose with real limits, and only cache what's actually worth remembering.

Source

“How and Why Netflix Built a Real-Time Distributed Graph: Part 3 - Querying the graph with gRPC execution API”

By Nilesh Mishra and Ajit Koti, on Netflix’s engineering blog

This page explains, in plain language, the architecture described in Netflix's own engineering blog post credited to Nilesh Mishra and Ajit Koti. All credit for the original work, research, and writing belongs to them and Netflix - this is our own explanation of the same publicly documented architecture, not a copy of their text.