Skip to content

Scaling: the ceilings

Every layer in the stack buys headroom by a different mechanism, and each one stops for a different reason. Knowing which ceiling you are actually hitting is most of the work — reaching for the wrong lever is how teams end up with a bigger cluster and the same wall time.

Layer Scales by Ceiling What you reach for next
numpy nothing; it is the baseline one array must fit in RAM chunking
xarray + dask splitting into chunks, computing chunk by chunk one machine's cores and memory bandwidth more machines
dask distributed adding worker processes and machines data movement, and the slowest single task a better graph, not more hardware
icechunk not throughput — correctness under concurrency commit contention on one branch separate branches, or fewer committers

The row that surprises people

Once work is genuinely distributed, adding hardware stops helping. Two limits bite, and neither is fixed by more machines.

Throughput tracks total threads, not worker count. Three workers with two threads each is six slots, and that is the number that governs a batch. dask-distributed/examples/0403_scaling.py measures the ladder — the same batch on 6 slots, then 4, then 2, at roughly 1.05s, 1.56s and 3.08s, very nearly the inverse of the slot count.

A batch can never finish faster than its longest single task. dask-distributed/examples/0503_task_stream.py catches a 1.50s straggler holding up a batch whose theoretical floor was 1.09s. When that happens the fix is splitting the task, not growing the cluster.

Diagnosing rather than guessing

The distributed scheduler exposes everything needed to tell these apart, and dask-distributed phase 5 reads it programmatically rather than by squinting at the dashboard:

  • Parallel efficiency — total compute time divided by (wall time x slots). Low efficiency with idle workers means the graph is too serial or the chunks are too big; low efficiency with busy workers means overhead is winning.
  • Time split by phase — every task record breaks into compute, transfer, and disk time. A task stream dominated by transfer means data is in the wrong place, which is a locality problem, not a capacity problem.
  • Per-worker balance — one worker doing most of the work is a placement problem; all workers equally busy but slow is a chunking problem.

0501_dashboard_tour.py prints each dashboard panel's underlying numbers, so "it looked busy" can be replaced with a figure.

Memory is a chunking problem

Each worker has a hard memory limit, and distributed escalates through four stages as it fills: spill to disk at 60%, spill aggressively at 70%, stop accepting tasks at 80%, and kill the worker at 95% (dask-distributed/examples/0301_worker_memory.py reads these from config and watches managed memory rise and fall).

The fix for memory pressure is almost never a bigger machine. A worker needs room for several chunks at once — roughly limit / threads — so the lever is chunk size, and unmanaged memory such as numpy scratch counts too. A persisted result nobody released is a leak that outlives the computation.