NativeLink

Tuning

The lever table: the signal you observe, the knob that moves it, the direction to turn it, and what turning it costs.

Who this is for: you have the production shape running and a number you don't like. What you'll have at the end: the specific knob for that number, and an honest account of what moving it costs.

Before you start

The production shape running (see Production configuration), and a number you don't like.

Tuning NativeLink is not a matter of turning up every dial. A small number of levers move real workloads; most of the config surface does not. Each row below starts from a signal you can actually observe (a metric, an error, a complaint), because starting from a knob and hoping is how afternoons disappear.

Change one thing

Every lever here has a cost column, and several of them trade against each other. Move one, watch the signal it targets for a full build cycle, then decide whether to move the next.

Storage and the CAS path

Signal you seeKnobDirectionExpected effectCost / risk
Cache hit rate falling, disk has headroommax_bytes in eviction_policyUpMore of the working set stays resident; hits go upDisk. Nothing else; it's a cache
CAS read latency high, disk is the bottleneckcontent_path / temp_pathMove to NVMeDirect latency win on every blobCost of the disk; temp_path must be on the same filesystem as content_path
Egress or storage bill dominated by blob volumecompression with lz4Wrap the terminal storeFewer bytes stored and sentCPU on every read and write through that store
Per-request object-store cost dominated by tiny blobssize_partitioningSplit at ~64k to 1MBSmall blobs go to a cheap fast store, large ones to the object storeTwo stores to size instead of one
Existence checks (FindMissingBlobs) dominating CAS loadexistence_cacheWrap the backendRepeat existence checks answered locallyStale negatives if the backend is mutated out of band; needs its own eviction_policy or it grows without bound
The same large artifacts uploaded repeatedly with small deltasdedupWrap with an index storeOnly changed chunks are storedChunk index store to run and size; more round trips per get()
One backend at its throughput ceilingshardAdd shardsLoad spreads by digest across backendsResharding changes which backend holds what; plan it as a migration
Read latency high but the working set is smallfast_slow hot tierSize fast to the 24 to 48h working setHot reads served locally, cold reads fall throughfast_slow never verifies the slow tier has everything; see the warning on production config
Worker disk filling with blobs it only readsfast_direction: "get"Set itLocal tier caches inputs without being written through on uploadOutputs are not locally cached; they go straight to the CAS
Page cache growth on a co-located filesystem-only deploymentevict_page_cacheAlmost always leave offKernel drops this store's pages after each read and writeSevere on many-core hosts; see below
StoreSpec

The evict_page_cache lever

The filesystem store has an opt-in evict_page_cache flag (default false). When enabled, after every blob it writes or reads it asks the kernel to drop that file's pages from the page cache (posix_fadvise(POSIX_FADV_DONTNEED)).

Most production deployments should leave it off. The common enterprise topology (an object store with multiple CAS nodes) never benefits from it, and enabling it carries two costs.

stores: [{
  name: "CAS_MAIN_STORE",
  filesystem: {
    content_path: "/var/lib/nativelink/content_path-cas",
    temp_path: "/var/lib/nativelink/tmp_path-cas",
    // Leave this off unless the narrow case below applies.
    evict_page_cache: false,
  },
}],

The second cost: it evicts the page cache. A store kept on a fast local disk that relies on the page cache as its hot read tier loses that cache after every read and write.

When to enable it: only for a filesystem-backed store on a low-core host where keeping this store's I/O out of the page cache is specifically desired (for example, to bound page-cache growth on a co-located filesystem-only deployment). It was originally added to relieve page-cache pressure that contributed to worker OOM-kills on such a deployment; note that page cache is reclaimable and the dominant OOM driver is a worker's anonymous memory (action processes and allocator retention), so this flag is a narrow mitigation, not a general OOM fix. Do not enable it on many-core hosts or object-store / multi-CAS deployments.

FilesystemSpec

Workers

Signal you seeKnobDirectionExpected effectCost / risk
Worker CPU idle while the queue is deepmax_inflight_tasksUp, toward core countMore actions run concurrently per workerMemory. Actions are real processes; oversubscribing invites the OOM killer. Default 0 means unlimited
Workers OOM-killed under loadmax_inflight_tasksDownFewer concurrent action processesLower per-worker throughput; you need more workers
Long link or EDA steps failing with a timeoutmax_action_timeout_sUpLong actions get to finishA genuinely hung action now holds a slot for longer. Default is 20 minutes
Zombie or leaked processes after buildsuse_namespaces + use_mount_namespaceOnEach action is contained and cleaned upLinux only; the container needs privileges to create namespaces
work_directory filling or slowwork_directoryMove to fast local disk, size for the largest actionFaster staging on every actionNever put it on network storage; it is the hot path of every action
Actions never dispatched to the right hardwareworker platform_propertiesAdd the propertyThe scheduler can route on itThe scheduler must list it too, or it rejects the worker's registration with Unknown platform property; see below
LocalWorkerConfig

Worker count itself is not a lever on this page; it is a scaling decision driven by nativelink:queue_depth, not by worker CPU.

Scheduler

Signal you seeKnobDirectionExpected effectCost / risk
Actions queued forever while workers sit idlesupported_platform_propertiesAdd the property the workers advertiseThe scheduler can match themA property listed here but absent on workers matches nothing; the two halves must agree
Spot / preemptible workers causing failed buildsmax_job_retriesUp from the default of 3Interrupted actions are retried elsewhereA deterministically failing action is now retried that many times before the client sees it
Healthy workers being dropped and re-registeringworker_timeout_sUp from the default of 5Fewer false drops on a slow or busy networkA genuinely dead worker is noticed later, so its actions are re-queued later
Actions stuck in executing on a wedged workermax_action_executing_timeout_sSet it (default 0 = disabled)Wedged actions are reclaimedToo low and long legitimate actions get killed mid-flight
Clients giving up before the scheduler doesclient_action_timeout_sAlign with client timeoutsScheduler and client agree on how long is too longDefault is 60 seconds
SimpleSpec

Process-level

Signal you seeKnobDirectionExpected effectCost / risk
Blob reads and writes stall under load, or Too many open files in the logglobal.max_open_filesUpMore concurrent blobs in flight (the binary gates file opens on a semaphore sized to 80% of the achieved limit)Capped by the hard ulimit -n; the binary defaults to 24576 when global is omitted
Redis connection saturationconnection_pool_size on redis_storeUp from the default of 3More concurrent Redis commandsMore connections against the Redis server's own limit
Redis commands timing outcommand_timeout_ms / connection_timeout_ms on redis_storeUp from 10000 / 3000Fewer spurious failures on a loaded RedisSlower failure detection. Use these, not the deprecated response_timeout_s / connection_timeout_s
High-latency links between clients and CASadvanced_http HTTP/2 window and stream settingsUpBetter throughput on fat, long pipesThese are hyper's knobs and mostly should be left alone; the reference config sets them only under a TODO
GlobalConfig RedisSpec

What is not a lever

Worth stating, because each of these gets reached for and none of them helps:

Worker CPU utilization as a scaling signal. A worker waiting on the CAS looks idle. Scale on nativelink:queue_depth instead.

Adding parallelism to work around evict_page_cache. It cannot work; see the callout above.

A metrics port. There isn't one. Telemetry is OTLP over gRPC via NL_OTEL_ENDPOINT, and cache metrics additionally require the cache_metrics store wrapper. Observability has the pipeline.

SidewaysRunbooks

Tuning is for a healthy system. When one of these signals is a symptom rather than a preference, start here instead.

NextObservability

The OTLP to Prometheus to Grafana pipeline that produces every signal in the left-hand column above.

On this page