Distributed Compute

Move the Computation to the Data

The intuition every programmer starts with is that you fetch the data and then work on it. At cluster scale that reverses: the code is kilobytes and the data is terabytes, so you ship the code to whichever machine already holds the bytes. Except when you should not — and the exceptions are more common every year.

▶ Run the lab

The question this answers

The question

Is it cheaper to move the data to the computation, or the computation to the data?

The guarantee — the property claimed, and its scope

None about correctness — locality is purely a performance and cost property, and a scheduler is free to ignore it. What it does offer is a bound: a task scheduled on a machine holding its input reads at local storage bandwidth; a task scheduled elsewhere reads at network bandwidth, which is a different number by a factor that depends entirely on your infrastructure.

Everything below is bought to hold this sentence. "Strongly consistent" with no scope attached is a slogan, not a guarantee — read what it actually covers, and what it explicitly does not.

What a node knows — observation versus inference

A scheduler knows where replicas of each input were *reported* to be, as of the last cluster report it received. It does not know the current cache state of any node, whether a machine is about to be pre-empted, or whether the "local" disk is a network-attached volume that will read over the same network anyway. Locality decisions are made on stale, partly fictional information — which is why they are treated as preferences rather than requirements.

A node knows its own state and the messages that arrived. Everything else is inference from evidence that was already stale. "B has not replied in five seconds" is knowledge; "B is down" is a decision — and usually the bug.

What guarantee?What does a node know?How does it work?What can fail?How does it fail?Where is coordination?What holds under failure?How does it recover?How would you know?What is the simpler thing?
localityplacementnetwork costdisaggregation

Why the intuition reverses

A program that processes a file normally reads the file. Scaled up, that becomes: a thousand workers each pull their slice across the network from wherever it lives, and the aggregate read is the entire data set moving over the network once per job. If the data is a hundred terabytes and the job runs hourly, that is a lot of network for a computation whose code is a few hundred kilobytes.

Turn it around. Put the workers on the machines that already hold the data, ship the code to them, and the only thing crossing the network is the code and the results. For a filtering or aggregating job the results are far smaller than the input, so total network traffic drops by orders of magnitude. This was the founding insight of the chunk-based file systems in Distributed File Systems: Chunks, a Metadata Service, and Where the Copies Go: chunks are placed on machines, jobs are scheduled onto those machines, and reading is a local disk read.

The reversal holds when three things are true: the data is much larger than the code, the computation reduces the data substantially, and local storage is genuinely faster than the network. Every one of those was overwhelmingly true in 2004. Two of them are still usually true. The third is exactly where the modern exceptions live.

LevelWhere the data isRead pathTypical relative cost
Process-localtypicalAlready in this process’s memoryNoneFree
Node-localtypicalOn this machine’s disk or page cacheLocal I/OCheap
Rack-localtypicalOn another machine in this rackOne switch hopModerate; shares the rack switch
Cluster-remoteassumptionAnother rack in the same datacentreRack uplink, often oversubscribedExpensive under load
Storage servicetypicalObject storage, same regionNetwork for every read, per-request chargeAlways network — locality is not available
Cross-regionprotocolAnother regionTens of milliseconds plus egress chargesAvoid; see [[speed-of-light]]
Locality levels, and what each one actually costs

Locality is a preference, and the delay-scheduling trade

A scheduler that insists on locality will sometimes have no local slot free, and then it has a choice: wait for one, or run the task somewhere else now. Both are wrong some of the time. Waiting leaves capacity idle; not waiting reads over the network.

Delay scheduling is the standard answer, and it is a nice piece of engineering. When a task’s preferred machines are busy, wait a short bounded time — a second or two — for a local slot, then fall back to rack-local, then to anywhere. Because tasks are short and slots free up constantly, a very small wait converts most assignments to local ones. The scheduler gets most of the benefit of insisting on locality with almost none of the idleness.

The lesson generalises past scheduling: a preference with a bounded fallback usually beats both a hard requirement and no preference at all. A hard requirement turns a performance optimisation into an availability risk — if the machines holding the data are down or full, the job does not run. No preference at all leaves a large, free win on the table. The bounded version captures the win and degrades rather than failing.

Delay scheduling: prefer local, fall back on a timer
yesnotimer expiresyesnoTask needs chunk 42Node holding chunk 42 free?Run node-local local disk readWait up to 2sRack-local slot free?Run rack-local one switch hopRun anywhere full network read
ClientGateway / LBServiceWorkerDatabaseCacheQueue / LogObject storageCDNExternal system

When locality does not hold

The rule has real exceptions, and knowing them is more valuable than knowing the rule, because the exceptions are the growing case.

The data is small. Moving a hundred megabytes is nothing. Constraining placement to chase it costs scheduling flexibility and buys a rounding error. Locality reasoning only pays when the data is large relative to everything else in the job.

The compute is heterogeneous. If the job needs a GPU and the data sits on general-purpose storage nodes, there is no local option and never will be. The same applies to any specialised resource: the placement is dictated by the scarce resource, and the data comes to it. This is the normal condition for model training, where the accelerators are the constraint and feeding them is a pipeline problem.

Compute and storage are already separated. This is the big one. When your data lives in object storage, every read is a network read for every worker, and there is no local option to prefer. Locality has not been violated; it simply does not exist as a concept in that architecture. The industry moved this way deliberately, and for good reasons — storage and compute scale independently, you stop paying for idle disks attached to busy CPUs, and you can run ephemeral or spot workers that hold no state. The price is that the founding assumption of data locality is gone.

The "local" disk is not local. A cloud instance’s attached volume is frequently a network-attached block service. A "node-local" read is a network read wearing a filename. Reasoning about locality against a diagram rather than against the actual storage path is a good way to optimise something that does not exist.

  • Small data: locality is a rounding error and constrains scheduling for nothing.
  • Heterogeneous compute: the scarce resource dictates placement, and the data comes to it.
  • Object storage: there is no local option — every read is a network read, by design.
  • Cloud volumes: "local disk" may be a network service, so verify before optimising.
  • Interactive workloads: scheduling delay to gain locality can cost more latency than the network read would have.

What replaces locality when locality is gone

Separating compute from storage does not repeal physics; it relocates the problem. If every read crosses the network, the techniques that matter become the ones that read less and the ones that read once.

Reading less is a data-layout question. Columnar formats let a query fetch three columns of two hundred; partition pruning skips whole directories; predicate pushdown and per-file statistics let a reader decline to fetch a file at all. These regularly cut bytes read by one or two orders of magnitude, which is a larger effect than locality ever provided.

Reading once is a caching question. A local SSD cache in front of remote storage restores something close to node-locality for repeated reads, and scheduling a task onto the node that already has its input cached is *locality again* — just against a cache rather than against the primary store. This is why cache-aware scheduling exists in engines that read from object storage: the concept survives, the thing it is measured against changes.

And the cost model changes shape. Locality used to be about time; with object storage it is also about money — per-request charges and, if the compute and the bucket are in different regions or clouds, egress. A job that reads across a region boundary can cost more in transfer than in compute, which is a category of bill that surprises people once each.

SELECT sum(amount) FROM events WHERE day = '2026-08-24' AND country = 'DE'

row-oriented, unpartitioned      read 4.2 TB   (everything)
columnar, unpartitioned          read  310 GB   (2 columns of 60)
columnar, partitioned by day     read  1.1 GB   (1 day, 2 columns)
  + file statistics on country   read  180 MB   (skips files with no DE rows)

Locality would have changed WHERE the 4.2 TB came from.
Layout changed whether it was read at all.
Same query, three layouts — bytes actually read

Key points

  • Code is small and data is large, so shipping computation to data beats shipping data to computation — when local storage is genuinely closer than the network.
  • Locality should be a preference with a bounded fallback, never a hard requirement; delay scheduling captures most of the win with almost none of the idleness.
  • It does not hold for small data, for heterogeneous compute, or for interactive work where the wait costs more than the read.
  • With object storage there is no local option at all — every read is a network read, and that is the deliberate design.
  • Once locality is gone, reading less (columnar, partition pruning, pushdown) and caching locally are what replace it.
  • A cloud "local disk" is often a network volume; verify the storage path before optimising against a diagram.

The chain, answered

Every field here is required, which is why no lesson in this domain can recommend a design without naming what an operator sees when it fails, what survives the partition, what repairs it afterwards, and the simpler thing to consider first.

How it works
  • The storage layer reports which machines hold each block, chunk or file.
  • The scheduler, given a task and its input, computes a preference list: process-local, node-local, rack-local, then anywhere.
  • It attempts to place the task at the best available level.
  • If no slot is free at the preferred level, it waits a short bounded time before relaxing to the next level.
  • The task runs and reads its input through whichever path its placement implies — local I/O, one switch hop, or a full network read.
  • The scheduler records the achieved locality level so the distribution can be observed and tuned.
What can fail at the boundary
  • The scheduler’s view of where data lives is stale after a rebalance or a machine loss.
  • The machines holding a popular input are saturated, so every task for it either waits or reads remotely.
  • Insisting on locality leaves capacity idle while tasks queue for specific machines.
  • The "local" volume is network-attached, so the optimisation buys nothing.
  • The data was moved to object storage and the scheduler’s locality logic silently becomes a no-op.
How it fails — what an operator sees
  • Idle cluster, queued tasks: utilisation is 40% and tasks are waiting, because locality is configured as a requirement and the preferred nodes are busy.
  • The locality cliff: a job’s runtime doubles after a storage rebalance, and the only changed metric is the node-local task fraction dropping from 90% to 30%.
  • Hot data node: one machine holds a popular input, its disk and network are pinned, and every job touching that input is slow while the rest of the cluster idles.
  • The optimisation that does nothing: an engineer tunes locality settings for a week with no effect, because the storage is object storage and there is no local option to win.
  • The egress invoice: compute in one region reads a bucket in another; the job is fine and the transfer line item is larger than the compute line item.
Where coordination is required
  • The scheduler needs a shared view of data placement, which is agreed state maintained by the storage layer and always somewhat stale.
  • Delay scheduling deliberately trades a small amount of coordination latency for a large reduction in network traffic — a good bargain because the wait is bounded and tasks are short.
  • Nothing about correctness requires coordination here: a task that reads remotely produces the same answer, just more slowly and more expensively.
  • Cache-aware scheduling reintroduces the same coordination against cache contents rather than primary storage, and cache contents are even more volatile.
What still holds under failure
  • Losing a machine costs locality for its data, not availability of it — other replicas serve the same bytes over the network.
  • A job whose locality collapses still completes, just slower; this is the reason locality must never be a hard constraint.
  • During a rebalance, achieved locality degrades cluster-wide and recovers as placement settles.
  • Achieved locality is a leading indicator of storage-layer trouble, since it drops before throughput does.
How it recovers
  • Detect: track the fraction of tasks achieving each locality level. A drop is visible before the runtime regression it causes.
  • Contain: keep locality a preference with a bounded wait so that degradation costs time rather than availability.
  • Recover: rebalance data or increase replication for hot inputs so more machines can serve them locally.
  • Reconcile: where locality has genuinely gone — object storage, GPUs — stop tuning it and switch effort to reading fewer bytes and caching what is re-read.
  • Verify: confirm the storage path is what you believe. Measure a local read’s throughput rather than assuming it from the mount point.
How you would know
  • Fraction of tasks at each locality level per job — the single metric that tells you whether locality is working at all.
  • Bytes read per task split into local and remote, which converts locality into a number you can price.
  • Scheduling delay attributable to waiting for a preferred slot, so the delay-scheduling trade can be tuned rather than guessed.
  • Per-machine read load on data nodes, which surfaces a hot input before it becomes a job-wide slowdown.
  • Bytes read from storage versus bytes the query actually needed — the layout metric, and the one that matters most once compute and storage are separated.
  • Cross-region and cross-cloud transfer bytes, because that is the cost that arrives as an invoice rather than as latency.
When it helps
  • Large scans over data stored on the same machines that run the compute — the classic on-premises analytics cluster.
  • Repeated jobs over the same inputs, where a local cache turns a network read into a local one after the first pass.
  • Any environment where the network is the constrained resource and storage bandwidth is not.
When it hurts
  • Small inputs, where the placement constraint costs more in scheduling flexibility than the read ever cost in bandwidth.
  • Specialised compute, where the scarce resource must dictate placement and locality is unattainable.
  • Object-storage architectures, where the concept does not apply and effort spent on it is wasted.
  • Interactive queries, where waiting for a local slot adds more latency than reading remotely would have.
Simpler alternatives
  • Read fewer bytes: columnar formats, partition pruning, predicate pushdown and file-level statistics beat locality outright in most modern setups.
  • Cache remote data on local SSD and schedule against the cache — locality restored against a different reference point.
  • Replicate hot inputs more widely so that more machines can serve them locally.
  • Move the computation into the storage layer itself, where the service supports it — the extreme form of shipping code to data.
  • Accept the network read and size the network for it, which is what a well-provisioned modern cluster does deliberately.

Move the computation to the data — and when reading less beats both

Move the computation to the data — except when you should not
Code is kilobytes and data is terabytes, so you ship the code. That argument has three premises, two of which are still true, and the third is exactly where the modern exceptions live.
where the input is
read time
50.00 s
vs node-local
1.00×
delay-scheduling wait
2.00 s
cluster utilisation
85%
Task needs chunk 42
  ├─ node holding chunk 42 free?  ── yes ──▶ run node-local (local disk read)
  │                                  no
  ├─ wait up to 2s ───── timer expires ──▶
  ├─ rack-local slot free?        ── yes ──▶ run rack-local (one switch hop)
  │                                  no
  └─ run anywhere (full network read)

node-local task fraction: 90%   cluster utilisation: 85%
Cheap. On this machine's disk or page cache, read over local i/o — 50.00 s against 50.00 s node-local. A watch worth keeping: a job’s runtime doubling after a storage rebalance, with the only changed metric being the node-local task fraction dropping from 90% to 30%. And verify the storage path first — a cloud “local disk” is often a network volume.
Where the data isRead pathTypical relative cost
Process-localtypicalAlready in this process's memoryNoneFree
Node-localtypicalOn this machine's disk or page cacheLocal I/OCheap
Rack-localtypicalOn another machine in this rackOne switch hopModerate; shares the rack switch
Cluster-remoteassumptionAnother rack in the same datacentreRack uplink, often oversubscribedExpensive under load
Storage servicetypicalObject storage, same regionNetwork for every read, per-request chargeAlways network — locality is not available
Cross-regionprotocolAnother regionTens of milliseconds plus egress chargesAvoid
Locality levels, and what each one actually costs.
assumptionThe cost ordering reflects a traditional cluster with local disks and an oversubscribed rack uplink. On a modern flat, high-bandwidth fabric the gap between node-local and rack-local can be small enough not to matter — and network-attached volumes and object storage break the premise entirely. Locality is a performance property only; no correctness guarantee depends on it.

What people believe, and what is true

Claim

Data locality is always the right optimisation.

Reality

It is right when data is large and local storage is faster than the network. With object storage neither the option nor the concept exists.

Claim

Separating compute and storage was a mistake because it loses locality.

Reality

It trades locality for independent scaling, ephemeral compute and no idle disks. The trade is usually favourable, and the lost locality is recovered through layout and caching.

Claim

Locality should be enforced.

Reality

Enforcing it converts a performance preference into an availability risk. Prefer with a bounded fallback.

Claim

My task reads from the local disk, so it is node-local.

Reality

On many cloud instances that disk is a network block service. Measure the read throughput before believing the mount point.

Claim

Once data is remote, nothing can be done.

Reality

Reading fewer bytes — columnar layout, pruning, pushdown — routinely wins more than locality ever did, and local caching restores much of the rest.

Go deeper

Only the levels this lesson can honestly fill — a missing level is a claim nobody had.

Overview

Code is small; data is big. Run the code where the data already is, and only the results cross the network. This reverses the usual instinct, and it stops applying when the data lives behind a network API anyway.

Practical

Track achieved locality per level and treat a drop as an early warning. Keep locality a preference with a short bounded wait. Before tuning it, confirm there is a local option at all — with object storage or network volumes there is not, and the effort belongs in file layout, pruning and caching instead.

Advanced

The principle underneath is that you move the smaller thing. Historically the code was always the smaller thing, so "move computation to data" was a universal rule. Disaggregated architectures did not repeal it; they changed what is small. A columnar projection of three columns is small. A predicate that eliminates ninety percent of files is small. A cache hit is nothing at all. Each of those is the same optimisation aimed at a different asymmetry, and reading a modern query engine this way — as a machine for making the moved thing smaller — explains most of what it does.

Apply it

Interview questions
  • 💬 Why does moving computation to data stop being the right instinct in a cloud data-lake architecture?
  • 💬 Why is locality a preference rather than a requirement in every real scheduler?
  • 💬 Your locality tuning has no effect at all. What would you check first?
  • 💬 What replaces locality once compute and storage are separated?