Decision matrix: sccache vs ccache on shared remote Mac pools
Pick one primary compiler cache per pipeline class. Mixing wrappers without isolating environment variables is how two jobs accidentally share the wrong CC launchers and silently poison statistics.
| Tool | Best when | Cross-border story | Watch-outs |
|---|---|---|---|
| sccache + Redis | You already operate TLS-capable Redis in each compliance region, and you compile Rust, Clang, or MSVC-shaped toolchains where sccache wrappers are standard | Namespace per product isolates keys; long-haul latency hits only wrapper I/O, not every object file write to NFS | Redis maxmemory eviction policies must match your willingness to rebuild; hot keys spike during monorepo merges |
| sccache + S3 (optional) | Legal prefers object-store retention over in-memory caches, or Redis is unavailable but signed S3 URLs are | Great for geographically sharded buckets; tune multipart thresholds and request concurrency per runner | Listing and small PUT storms can dominate bill unless you batch; still pair with local SCCACHE_DIR on NVMe |
| ccache + local APFS | Single-tenant dedicated hosts where every job can keep a large CCACHE_DIR on internal SSD |
Cross-border benefit is indirect: you avoid remote object traffic entirely when the runner stays beside developers | Ephemeral disks erase warmth unless you rehydrate from an artifact stage you control |
| ccache + NFS or HTTP secondary storage | Homogeneous Clang builds and storage teams already expose POSIX or HTTP caches inside the same metro | Multinational hits only work when latency to the filer stays low; otherwise metadata RTT dominates | Requires tuned mount options, local CCACHE_TEMPDIR, and realistic CCACHE_MAXSIZE to avoid brownouts |
Neither tool replaces a disciplined toolchain pin: bumping Xcode or Swift driver versions changes preprocessor fingerprints, so treat cache hit ratio dashboards as signals tied to your compiler matrix, not vanity metrics.
Parameter checklist: Redis endpoints, key prefixes, NFS, concurrency
Redis endpoint hygiene: configure SCCACHE_REDIS_ENDPOINT with rediss:// when TLS terminates on the broker, split credentials into SCCACHE_REDIS_USERNAME and SCCACHE_REDIS_PASSWORD instead of embedding secrets in URLs, and keep ACL users scoped to a single SCCACHE_REDIS_DB index. Document upstream timeout and tcp-keepalive so CI authors know why wrappers stall. Pair every endpoint with a key prefix via SCCACHE_REDIS_KEY_PREFIX (and optional SCCACHE_NAMESPACE for multi-backend setups) so marketing experiments never collide with kernel-driver builds.
NFS mounts for ccache: mount with noatime, align rsize/wsize with vendor guidance, and avoid crossing oceans for metadata-heavy phases—run a compile microbenchmark before you promise SLA. Always set CCACHE_TEMPDIR to a job-local folder on the Mac’s internal SSD so temporary objects never land on the filer.
Concurrent workers: cap CMAKE_BUILD_PARALLEL_LEVEL or Ninja -j relative to RAM; each extra linker job increases peak RSS and can starve the wrapper’s ability to push entries to Redis. Start conservative (for example half of performance cores) on shared remote Mac hosts, then scale up while watching sccache --show-stats error counters.
- Committed runbook lists the canonical Redis host, port, DB index, ACL name, and rotation procedure.
SCCACHE_REDIS_KEY_PREFIX(and any globalSCCACHE_NAMESPACE) encodes product, major Xcode stream, and intentional breaking generation.SCCACHE_DIRandCCACHE_DIRpaths never overlap between tenants without separate subdirectories.- NFS exports use snapshots or backups compatible with your eviction policy; operators know who clears stuck locks.
- CI surfaces
sccache --show-statsorccache -sin build logs after the compile phase. - Disk watermarks exist for both local wrapper staging and shared filers, with alerts before jobs fail mid-link.
Executable environment variables, cache directories, timeouts, retries
Copy the block that matches your chosen stack, then adjust hostnames. Keep secrets in your vault—only non-secret identifiers belong in logs.
# --- sccache + Redis (TLS; secrets via dedicated vars) ---
export SCCACHE_REDIS_ENDPOINT="rediss://sccache-redis.internal:6379"
export SCCACHE_REDIS_USERNAME="ci-sccache"
export SCCACHE_REDIS_PASSWORD="${SCCACHE_REDIS_PASSWORD_SECRET}" # inject from vault
export SCCACHE_REDIS_DB="0"
export SCCACHE_REDIS_KEY_PREFIX="acme/mobile/xcode-16-3/"
export SCCACHE_REDIS_EXPIRATION="2592000" # optional: 30d TTL in seconds
# Optional global namespace when mixing backends in one job
export SCCACHE_NAMESPACE="acme-ci"
# Local staging on APFS (per job directory on shared hosts)
export SCCACHE_DIR="${CI_PROJECT_DIR}/.sccache-staging"
export SCCACHE_IDLE_TIMEOUT="1800" # seconds; raise if link steps exceed 15m
export SCCACHE_CACHE_SIZE="64G" # local disk cap for sccache’s on-disk pieces
export RUSTC_WRAPPER="$(command -v sccache)"
export CARGO_BUILD_RUSTC_WRAPPER="${RUSTC_WRAPPER}"
# CMake / Clang
export CMAKE_C_COMPILER_LAUNCHER="$(command -v sccache)"
export CMAKE_CXX_COMPILER_LAUNCHER="$(command -v sccache)"
mkdir -p "${SCCACHE_DIR}"
# --- ccache + APFS (job-local) + bounded size ---
export CCACHE_DIR="${CI_PROJECT_DIR}/.ccache"
export CCACHE_TEMPDIR="${TMPDIR:-/tmp}/ccache-${CI_JOB_ID:-local}"
export CCACHE_MAXSIZE="32G"
export CCACHE_LIMIT_MULTIPLE="0.85"
export CCACHE_COMPRESS="1"
export CCACHE_COMPRESSLEVEL="6"
export CCACHE_SLOPPINESS="pch_defines,time_macros"
export CMAKE_C_COMPILER_LAUNCHER="$(command -v ccache)"
export CMAKE_CXX_COMPILER_LAUNCHER="$(command -v ccache)"
mkdir -p "${CCACHE_DIR}" "${CCACHE_TEMPDIR}"
# --- Retry ladder for flaky wrapper transport (call after exports) ---
run_with_cache_retries() {
local attempt=1 max=4 delay=2
while [ "${attempt}" -le "${max}" ]; do
echo "compile phase attempt ${attempt}/${max}"
if "$@"; then return 0; fi
sleep "${delay}"
delay=$((delay * 2))
attempt=$((attempt + 1))
done
return 1
}
# Example: run_with_cache_retries cmake --build build --parallel "${CMAKE_BUILD_PARALLEL_LEVEL:-8}"
Timeout hints: keep Redis client timeouts aligned with your slowest expected LTO unit; if Redis disconnects every nine minutes while links last twelve, raising SCCACHE_IDLE_TIMEOUT matters more than adding raw -j. For HTTP secondary storage in ccache 4.x, mirror vendor defaults for connect and read timeouts, then validate with a synthetic cold build from each region you support.
FAQ
Does sccache replace distcc? No. sccache stores compilation results; it does not schedule remote compile farms. You can combine concepts, but this article focuses on cache hit economics, not distributed compile topology.
Should I enable every ccache sloppiness flag to chase hits? Only flags your security reviewers accept. Sloppiness trades reproducibility for hit rate—document the exact set next to your SBOM policy.
What if Redis memory pressure evicts hot entries mid-sprint? Treat eviction as normal, widen namespaces only when you intend to isolate, and schedule a nightly warm job that rebuilds the top ten targets so developers do not feel random pain.
Where do Xcode-derived data caches fit? They solve a different layer than compiler wrappers; pair this guide with the Xcode DerivedData cache matrix when optimizing full iOS pipelines.
Summary
Redis-backed sccache wins when multinational runners must share one governed object namespace; ccache on APFS with optional NFS or HTTP secondary storage wins when storage compliance is POSIX-shaped and latency to the filer is boringly low. Document key prefixes, timeouts, and retry ladders beside your compiler matrix so cache regressions read like infrastructure incidents, not mystery flakes.
When you are ready to place dedicated Apple Silicon capacity beside your cache tier, compare pricing, purchase options, and the help center on macpull.com—all viewable without signing in—then return to the blog for build-pool tuning patterns.