With Xcode 26, Apple has finally introduced Xcode Compilation Caching: a major leap forward for iOS developers, who no longer have to wait around for slow builds while their CI pipelines recompile code that hasn’t changed. At Bitrise, our Build Cache product already supports Bazel and Gradle, so the logical next step was to introduce support for compilation caching in Xcode. In this blog post, we’re lifting the hood on how we built this new feature.
For more context on why and how Build Cache for Xcode is a game-changer for iOS teams, check out the announcement blog.
Xcode and LLVM
The newest addition to the Xcode build system uses LLVM Content Addressable Storage (CAS) to store previously built outputs. The granularity of these outputs is below the function level, and this mechanism works transparently under the hood. Let’s explore it a bit!
With compilation caching enabled, the compilation process proceeds as usual except that, before performing the actual compilation, the build system queries the cache to see if the outputs already exist. For each module or file (compiled code or artifact), a cache key is generated and used to query the cache. This key/value API determines which files are already stored in the cache. This whole CAS concept in LLVM is very similar to Bazel's action cache concept.
To download binary data, the storage is accessed using the hash of the object as the key, hence the name Content Addressable Storage (CAS). This indirection lets the system store identical content for multiple cache keys without duplication.
Conversely, if the content is not found in the cache, Xcode performs the build step, saves the content using its hash, and then associates that hash with the key for the current compilation step for later retrieval.
Example cache hit:
[Bitrise Build Cache] [11:55:57] GetValue called with key: xcelerate-kv-000277c16f366e0a1a7cc1405e44911f2fabbc620bd93f3cede6f3dfedcecd5dcb5040dd7633038fda0b9e37242b16972cf84059e7b19913c5f9c4c770a8d631ff
[Bitrise Build Cache] [11:55:57] GetValue with key xcelerate-kv-000277c16f366e0a1a7cc1405e44911f2fabbc620bd93f3cede6f3dfedcecd5dcb5040dd7633038fda0b9e37242b16972cf84059e7b19913c5f9c4c770a8d631ff took 135.287ms and was a hit: true
[11:56:09] PrecompileModule /Users/zsolt/Library/Developer/Xcode/DerivedData/WordPress-bpdocefeglnuxjgkqgfoewfbhzra/Build/Intermediates.noindex/ArchiveIntermediates/WordPress/IntermediateBuildFilesPath/ExplicitPrecompiledModules/_Builtin_float-5ZAOIK7Z528RU7VV3KIU82N5D.scan
[11:56:09] cd /
[11:56:09] builtin-precompileModule /Users/zsolt/Library/Developer/Xcode/DerivedData/WordPress-bpdocefeglnuxjgkqgfoewfbhzra/Build/Intermediates.noindex/ArchiveIntermediates/WordPress/IntermediateBuildFilesPath/ExplicitPrecompiledModules/_Builtin_float-5ZAOIK7Z528RU7VV3KIU82N5D.scan
[11:56:09] note: replayed cache hit: 0~AnfBbzZuChp8wUBeRJEfL6u8YgvZPzzt5vPf7c7NXctQQN12MwOP2gueNyQrFpcs-EBZ57GZE8X5xMdwqNYx_w==
[11:56:09] note: using CAS output deps: 0~eGoC90IBWQPGxv2FJVLScpEvR0DhWEdhiobiF_cfVBnSXhAxr-5YUxOJZESTTrBLkDpoWxRIt1XVb3Aa_pvizg==
[11:56:09] note: using CAS output main: 0~DY7ymr6tq-UFu4az2gogLElvA3MKIn75eyzIj4tyk5wnJaqFHjzNwCddU2ps-05H94TmT2l5UXAwtW0WLkAQrw==
[11:56:09] Cache hitXcode uses a gRPC-based protocol (defined in Apple’s fork of LLVM) to query the cache (key-value API) for hashes and to get/put objects in the CAS. By default, cached data on disk is stored under DerivedData/CompilationCache.noindex, but you can register a custom remote cache server instead. Note: the cache stores individual blobs on demand, not a copy of the entire directory.
Also note that the clean command does not remove the compilation cache folder, unlike other DerivedData artifacts. If the DerivedData folder already contains a build output, Xcode will use the local files directly and will not query the cache for that output, resulting in 0% remote cache hit rate!
How to enable
To enable Xcode Compilation Caching (XCC), we need to enable these via environment variables/flags/project settings:
- "COMPILATION_CACHE_ENABLE_PLUGIN": "YES",
- "SWIFT_ENABLE_COMPILE_CACHE": "YES",
- "CLANG_ENABLE_COMPILE_CACHE": "YES",
We also have to enable Swift integrated driver and explicit modules.
- "SWIFT_ENABLE_EXPLICIT_MODULES": "YES",
- "SWIFT_USE_INTEGRATED_DRIVER": "YES",
- "CLANG_ENABLE_MODULES": "YES",
Finally, to provide the cache service we also need to set the socket of our gRPC server with COMPILATION_CACHE_REMOTE_SERVICE_PATH as an additional argument.
Cache service
Since we already provide Build Cache for Gradle and Bazel, it came naturally to reuse the same service to store the LLVM key-values and CAS content. Moreover, Bazel already has a concept of CAS, so really we just had to reuse the same mechanism.
We have built a local proxy that creates key hashes from binary data for CAS and relays all calls to our cache backend service. It also automatically makes the cache usage data available in Bitrise Insights. This proxy acts as a middle man between Xcode and our cache backend by implementing the LLVM gRPC server mentioned above.
This local cache proxy needs to run alongside Xcode builds all the time, which means an extra setup required. But don’t worry, we even took another step and automated this!
Bitrise CLI wrapper and steps
To make caching smoother on the CI and locally, we created a wrapper around the xcodebuild tool to inject the setup for compilation caching into all xcode invocations automatically.
The wrapper has a bunch of responsibilities:
- Automatically adds the compilation cache flags
- Ensures that the local proxy is running to connect to the remote cache
- Transparently calls the original xcodebuild implementation
- Streams logs and return exit code from the xcode build process
- Saves invocation data to seamlessly integrate with Bitrise analytics features such as invocation details or invocation compare
This wrapper is actually part of the Bitrise Build Cache CLI as a subcommand. When enabling build caching through the activate xcode command, the CLI saves the configuration provided (e.g. authentication) and then copies itself into the home directory ~/.bitrise-xcelerate/bin.
To make the integration effortless, it also modifies the PATH in ~/.zshrc and ~/.bashrc so that all xcodebuild commands actually call our wrapper. This works also for Github Actions ($GITHUB_PATH) and on Bitrise CI using envman.
Example invocation:
❯ xcodebuild "archive" "-workspace" "WordPress.xcworkspace" "-scheme" "WordPress" "-configuration" "Debug" "-destination" "generic/platform=iOS Simulator" "CODE_SIGN_IDENTITY=" "CODE_SIGNING_REQUIRED=NO"
+ /Users/zsolt/.bitrise-xcelerate/bin/bitrise-build-cache-cli xcelerate xcodebuild archive -workspace WordPress.xcworkspace -scheme WordPress -configuration Debug -destination 'generic/platform=iOS Simulator' CODE_SIGN_IDENTITY= CODE_SIGNING_REQUIRED=NO
ℹ️ These logs are available at: /Users/zsolt/.local/state/xcelerate/logs/xcelerate-3b4a6de8-f84e-4f3a-b69b-19e1237acd4e.log
[Bitrise Analytics] [10:01:38] Cache enabled, starting xcelerate proxy connecting to: grpcs://bitrise-accelerate.services.bitrise.io
[Bitrise Analytics] [10:01:38] Started xcelerate_proxy pid = 3342
[Bitrise Analytics] [10:01:38] Connecting to proxy socket: unix:///var/folders/4h/11t9npjn3ldbxl1c9g7dzq140000gn/T/xcelerate-proxy.sock
[Bitrise Build Cache] [10:01:38] SetSession called with invocation ID: 3b4a6de8-f84e-4f3a-b69b-19e1237acd4e
Command line invocation:
/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild archive -workspace WordPress.xcworkspace -scheme WordPress -configuration Debug -destination "generic/platform=iOS Simulator" CODE_SIGN_IDENTITY= CODE_SIGNING_REQUIRED=NO COMPILATION_CACHE_ENABLE_DIAGNOSTIC_REMARKS=NO SWIFT_ENABLE_COMPILE_CACHE=YES SWIFT_USE_INTEGRATED_DRIVER=YES CLANG_ENABLE_MODULES=YES COMPILATION_CACHE_ENABLE_PLUGIN=YES COMPILATION_CACHE_ENABLE_INTEGRATED_QUERIES=YES COMPILATION_CACHE_ENABLE_DETACHED_KEY_QUERIES=YES SWIFT_ENABLE_EXPLICIT_MODULES=YES CLANG_ENABLE_COMPILE_CACHE=YES COMPILATION_CACHE_REMOTE_SERVICE_PATH=/var/folders/4h/11t9npjn3ldbxl1c9g7dzq140000gn/T/xcelerate-proxy.sock
Build settings from command line:
CLANG_ENABLE_COMPILE_CACHE = YES
CLANG_ENABLE_MODULES = YES
CODE_SIGN_IDENTITY =
CODE_SIGNING_REQUIRED = NO
COMPILATION_CACHE_ENABLE_DETACHED_KEY_QUERIES = YES
COMPILATION_CACHE_ENABLE_DIAGNOSTIC_REMARKS = NO
COMPILATION_CACHE_ENABLE_INTEGRATED_QUERIES = YES
COMPILATION_CACHE_ENABLE_PLUGIN = YES
COMPILATION_CACHE_REMOTE_SERVICE_PATH = /var/folders/4h/11t9npjn3ldbxl1c9g7dzq140000gn/T/xcelerate-proxy.sock
SWIFT_ENABLE_COMPILE_CACHE = YES
SWIFT_ENABLE_EXPLICIT_MODULES = YES
SWIFT_USE_INTEGRATED_DRIVER = YESNote: this wrapping does not support the Xcode UI as it uses an internal service to perform the build steps and we don’t have a central entrypoint we can hijack. This is an important difference to Gradle, where an init script in the home directory is called by Gradle on every invocation.
Finally, as the top layer we have built a Bitrise Step for Bitrise CI that calls the Build Cache CLI to enable Build Cache for Xcode using the activate xcode command.

Conclusion
In short, Xcode’s new CAS-based compilation cache changes the economics of builds: identical compilation outputs are stored once and reused across builds, which cuts redundant CPU time and shortens iteration cycles for teams. For CI, that means faster pipelines and lower infrastructure bills. Locally, it gives developers quicker compile-test cycles.
To get these gains, just add the Build Cache for Xcode step in your CI workflow, and all wrapping will be automatically enabled. You can then track your builds’ performance in Bitrise Insights, and compare invocations to get quick insights into performance bottlenecks.



