The question this answers
Is it cheaper to move the data to the computation, or the computation to the data?
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.
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.
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.
| Level | Where the data is | Read path | Typical relative cost |
|---|---|---|---|
| Process-localtypical | Already in this process’s memory | None | Free |
| Node-localtypical | On this machine’s disk or page cache | Local I/O | Cheap |
| Rack-localtypical | On another machine in this rack | One switch hop | Moderate; shares the rack switch |
| Cluster-remoteassumption | Another rack in the same datacentre | Rack uplink, often oversubscribed | Expensive under load |
| Storage servicetypical | Object storage, same region | Network for every read, per-request charge | Always network — locality is not available |
| Cross-regionprotocol | Another region | Tens of milliseconds plus egress charges | Avoid; see [[speed-of-light]] |
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.
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.
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.
- • 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.
- • 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.
- • 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.
- • 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.
- • 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.
- • 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.
- • 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.
- • 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.
- • 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.
- • 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
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%
| Where the data is | Read path | Typical relative cost | |
|---|---|---|---|
| Process-localtypical | Already in this process's memory | None | Free |
| Node-localtypical | On this machine's disk or page cache | Local I/O | Cheap |
| Rack-localtypical | On another machine in this rack | One switch hop | Moderate; shares the rack switch |
| Cluster-remoteassumption | Another rack in the same datacentre | Rack uplink, often oversubscribed | Expensive under load |
| Storage servicetypical | Object storage, same region | Network for every read, per-request charge | Always network — locality is not available |
| Cross-regionprotocol | Another region | Tens of milliseconds plus egress charges | Avoid |
What people believe, and what is true
Data locality is always the right optimisation.
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.
Separating compute and storage was a mistake because it loses locality.
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.
Locality should be enforced.
Enforcing it converts a performance preference into an availability risk. Prefer with a bounded fallback.
My task reads from the local disk, so it is node-local.
On many cloud instances that disk is a network block service. Measure the read throughput before believing the mount point.
Once data is remote, nothing can be done.
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
- 💬 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?