Claude Code for Bazel Remote Cache (2026)
Claude Code for Bazel Remote Cache Workflow
Bazel’s incremental build capabilities are powerful, but even the fastest local builds can become bottlenecks in CI/CD pipelines and team environments. Remote caching transforms your build economy by sharing compilation artifacts across machines, and Claude Code can help you set up, manage, and optimize this workflow. This guide shows you how to integrate Claude Code into your Bazel remote cache setup for faster, more efficient builds.
Understanding Bazel Remote Caching
Bazel remote caching stores build outputs on a remote server instead of (or in addition to) your local machine. When another developer or CI runner needs a build artifact, Bazel downloads it from the cache instead of rebuilding from source. This dramatically reduces build times, especially for large monorepos with many interdependent targets.
There are two primary remote cache backends compatible with Bazel:
- Remote Build Execution (RBE): Both caches and executes builds remotely
- Remote Cache (RC): Only caches outputs, local execution
For most teams starting with remote caching, the cache-only approach is simpler to implement and provides immediate benefits without the complexity of remote execution.
Remote Cache vs. Disk Cache vs. RBE
Understanding which caching strategy fits your team is the first step before writing a single line of configuration.
| Feature | Disk Cache | Remote Cache | Remote Build Execution |
|---|---|---|---|
| Shared across machines | No | Yes | Yes |
| Setup complexity | Low | Medium | High |
| Requires network | No | Yes | Yes |
| Execution offloading | No | No | Yes |
| Best for | Solo developers | Small–large teams | Large teams, CI scale |
| Typical cache hit rate | 40–60% | 70–90% | 80–95% |
| Cost | Free | Server cost | Higher infrastructure cost |
Most teams see the biggest immediate win from remote caching without RBE. Once your hit rate plateaus and build queue times become the bottleneck, migrating to RBE is the natural next step.
How Bazel Computes Cache Keys
Bazel’s cache key for any action is a hash of:
- The action’s command line
- All declared input files (content-addressed, not timestamp-based)
- The environment variables declared in the action
- The execution platform and toolchain identifiers
This design means two machines with identical inputs always produce the same cache key. However, any undeclared dependency. a file read from disk without being declared in srcs or data, or an environment variable injected at build time. will cause cache misses or, worse, silently incorrect builds.
Claude Code is particularly useful here because it can audit your BUILD files for common patterns that leak undeclared inputs.
Setting Up Your Remote Cache
The most common remote cache implementations use either a gRPC-based protocol or HTTP/1.1. Here’s how to configure Bazel to use a remote cache with Claude Code assisting you:
Configuring the Bazelrc File
Create or modify your .bazelrc file to enable remote caching:
build --remote_cache=https://your-cache-server.example.com
build --remote_cache_header=Authorization Bearer YOUR_TOKEN
build --disk_cache=~/.bazel/cache
The disk_cache setting provides a local fallback, ensuring you have some caching even when the remote is unavailable.
A production-grade .bazelrc will separate concerns using config groups so engineers can opt in to remote caching without forcing it on every build:
.bazelrc
Always-on: local disk cache as a fallback
build --disk_cache=~/.bazel/cache
Opt-in remote cache (use: bazel build --config=remote //...)
build:remote --remote_cache=grpcs://your-cache-server.example.com:443
build:remote --remote_cache_header=Authorization=Bearer ${BAZEL_REMOTE_TOKEN}
build:remote --remote_timeout=30s
build:remote --remote_retries=2
CI profile: remote cache always enabled, uploads allowed
build:ci --config=remote
build:ci --remote_upload_local_results=true
Dev profile: remote cache for downloads only, no uploads
build:dev --config=remote
build:dev --remote_upload_local_results=false
Breaking the configuration this way achieves two goals. First, local developer builds stay fast when the remote cache is degraded. Second, only CI runners can write to the remote cache, which prevents half-built developer artifacts from poisoning the shared cache.
Setting Up a Simple Cache Server
For teams wanting to self-host, several options exist. The Bazel-Build-Event-Service (BES) can serve as a basic cache, or you can use specialized tools like Buildbarn or EngFlow. Claude Code can help you generate the appropriate Docker compose configuration:
version: '3'
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
bazel-cache:
image: buildbarn/bb-browser
ports:
- "8080:8080"
volumes:
- cache-data:/data
For a more production-ready self-hosted solution, bazel-remote is the most commonly used standalone cache server. Here is a more complete Docker Compose setup that includes authentication and eviction settings:
version: '3.8'
services:
bazel-remote:
image: buchgr/bazel-remote-cache:latest
ports:
- "9090:9090" # gRPC
- "8080:8080" # HTTP
command:
- --dir=/data
- --max_size=50 # GB
- --htpasswd_file=/etc/bazel-remote/.htpasswd
- --tls_cert_file=/certs/server.crt
- --tls_key_file=/certs/server.key
volumes:
- cache-data:/data
- ./certs:/certs:ro
- ./.htpasswd:/etc/bazel-remote/.htpasswd:ro
restart: unless-stopped
volumes:
cache-data:
driver: local
driver_opts:
type: none
device: /mnt/fast-ssd/bazel-cache
o: bind
The --max_size flag enables LRU eviction so the cache disk usage stays bounded. Mounting the cache data directory on a fast SSD (or NVMe) significantly reduces the latency for cache reads.
Managed Remote Cache Options
If operating your own server is not practical, several managed options integrate with Bazel out of the box:
| Provider | Protocol | Auth | Free Tier |
|---|---|---|---|
| BuildBuddy Cloud | gRPC + HTTP | API key | Yes (limited) |
| EngFlow | gRPC | OIDC | No |
| Google Cloud Storage | HTTP | Service account | Pay-per-use |
| AWS S3 (via bazel-remote) | HTTP | IAM | Pay-per-use |
| Gradle Enterprise | gRPC | Token | No |
For teams already on GCP, using Google Cloud Storage as the cache backend requires zero additional infrastructure. Add the following to your .bazelrc and authenticate via Application Default Credentials:
build:remote --remote_cache=https://storage.googleapis.com/YOUR_BUCKET_NAME
build:remote --google_default_credentials
Creating a Claude Code Skill for Cache Management
A Claude Code skill can automate common cache operations, diagnose issues, and help optimize your cache hit rates. Here’s a skill structure for Bazel cache management:
Cache Status Skill
---
name: bazel-cache-status
description: Check and analyze Bazel remote cache status
---
Bazel Cache Status Checker
Check the current remote cache configuration and test connectivity.
Check Cache Configuration
Run this command to see your current cache settings:
The skill would then guide users through commands like:
bazel info | grep -i cache
bazel clean --expunge
A more complete version of this skill includes connectivity testing and configuration validation in a single sweep:
#!/usr/bin/env bash
cache-health.sh. run this via: claude --print "run cache-health.sh and summarize"
set -euo pipefail
CACHE_URL="${BAZEL_REMOTE_CACHE_URL:-}"
if [[ -z "$CACHE_URL" ]]; then
echo "ERROR: BAZEL_REMOTE_CACHE_URL not set"
exit 1
fi
echo "=== Bazel Cache Health Check ==="
echo ""
echo "--- Current bazelrc cache settings ---"
bazel info 2>/dev/null | grep -E "cache|remote" || echo "(no cache info)"
echo ""
echo "--- Connectivity test ---"
if curl -sf --max-time 5 "${CACHE_URL}/health" > /dev/null; then
echo "PASS: cache server reachable"
else
echo "FAIL: cannot reach ${CACHE_URL}"
fi
echo ""
echo "--- Disk cache size ---"
DISK_CACHE="${HOME}/.bazel/cache"
if [[ -d "$DISK_CACHE" ]]; then
du -sh "$DISK_CACHE"
else
echo "No local disk cache found at ${DISK_CACHE}"
fi
echo ""
echo "--- Recent build summary (last 5 builds) ---"
if [[ -f build_events.json ]]; then
python3 - <<'PYEOF'
import json, sys
with open("build_events.json") as f:
events = [json.loads(line) for line in f if line.strip()]
hits = sum(1 for e in events if e.get("id", {}).get("actionCompleted") and
e.get("action", {}).get("type") == "MiddleMan" is False and
e.get("action", {}).get("failureDetail") is None and
"CacheHit" in str(e))
total = sum(1 for e in events if "actionCompleted" in str(e.get("id", {})))
print(f"Approximate cache hits: {hits}/{total}")
PYEOF
else
echo "No build_events.json found. run with --build_event_json_file=build_events.json"
fi
Cache Hit Rate Analysis
Understanding your cache hit rate is crucial for optimization. Create a skill that parses build event logs to report cache performance:
def analyze_cache_performance(build_log_path):
"""Parse Bazel build events to calculate cache hit rate."""
with open(build_log_path, 'r') as f:
events = json.load(f)
total_actions = 0
cache_hits = 0
for event in events:
if 'action' in event:
total_actions += 1
if event['action'].get('cached'):
cache_hits += 1
hit_rate = (cache_hits / total_actions * 100) if total_actions > 0 else 0
return f"Cache hit rate: {hit_rate:.1f}%"
Extending this to produce actionable output for Claude to analyze:
import json
import sys
from collections import defaultdict
from pathlib import Path
def analyze_cache_performance(build_log_path: str) -> dict:
"""
Parse a Bazel Build Event Protocol JSON file and return
per-mnemonic cache hit rates for Claude Code to interpret.
"""
path = Path(build_log_path)
if not path.exists():
return {"error": f"File not found: {build_log_path}"}
per_mnemonic: dict[str, dict[str, int]] = defaultdict(lambda: {"hits": 0, "misses": 0})
total_actions = 0
cache_hits = 0
with open(path) as f:
for line in f:
line = line.strip()
if not line:
continue
try:
event = json.loads(line)
except json.JSONDecodeError:
continue
action = event.get("action", {})
if not action:
continue
total_actions += 1
mnemonic = action.get("type", "Unknown")
if action.get("cached") or action.get("cacheHit"):
cache_hits += 1
per_mnemonic[mnemonic]["hits"] += 1
else:
per_mnemonic[mnemonic]["misses"] += 1
overall_rate = (cache_hits / total_actions * 100) if total_actions > 0 else 0
# Sort by miss count descending so the worst offenders appear first
sorted_mnemonics = sorted(
per_mnemonic.items(),
key=lambda kv: kv[1]["misses"],
reverse=True
)
return {
"overall_hit_rate": f"{overall_rate:.1f}%",
"total_actions": total_actions,
"cache_hits": cache_hits,
"worst_offenders": [
{
"mnemonic": m,
"hits": v["hits"],
"misses": v["misses"],
"hit_rate": f"{v['hits'] / (v['hits'] + v['misses']) * 100:.1f}%"
}
for m, v in sorted_mnemonics[:10]
]
}
if __name__ == "__main__":
result = analyze_cache_performance(sys.argv[1] if len(sys.argv) > 1 else "build_events.json")
print(json.dumps(result, indent=2))
You can then pass this script’s output directly to Claude Code for interpretation and recommendations:
python3 analyze_cache.py build_events.json | \
claude --print "Analyze these Bazel cache metrics and suggest the top 3 improvements"
Practical Workflows with Claude Code
Workflow 1: Initial Repository Setup
When setting up a new repository with Bazel and remote caching, Claude Code can guide you through the complete process:
- Initialize the Bazel workspace with
bazel init - Configure the
.bazelversionfile - Set up the remote cache in
.bazelrc - Verify connectivity with a test build
- Document the setup in your team’s README
Here is a concrete example of what the initial workspace looks like after Claude Code has scaffolded it. The MODULE.bazel file (Bzlmod format, Bazel 6+) is typically the trickiest part for new users:
MODULE.bazel
module(
name = "my_project",
version = "0.1",
)
bazel_dep(name = "rules_go", version = "0.46.0")
bazel_dep(name = "gazelle", version = "0.35.0")
bazel_dep(name = "rules_python", version = "0.31.0")
And the companion .bazelversion that pins the exact Bazel release:
7.1.0
Pinning the Bazel version is important for cache correctness. If different developers run different Bazel versions, the toolchain identifier changes and cache keys diverge, causing every CI build to rebuild from scratch.
Workflow 2: Debugging Cache Misses
When builds aren’t caching as expected, Claude Code can help diagnose common issues:
- Unmatched inputs: Check for timestamp-based or random inputs in build rules
- Toolchain differences: Ensure consistent toolchains across machines
- Action inputs: Review
bazel aqueryoutput to see what inputs Bazel considers
Query what inputs an action uses
bazel aquery '//some:target' --output=json | jq '.actions[].inputDepSets[]'
A systematic debugging session with Claude Code might look like this:
Step 1: Check whether the cache key changes between two runs
bazel aquery '//your/package:target' --output=text 2>&1 | sha256sum
Step 2: If the hash changes, find which input changed
bazel aquery '//your/package:target' --output=text > run1.txt
Make no changes, rebuild
bazel aquery '//your/package:target' --output=text > run2.txt
diff run1.txt run2.txt
Step 3: Look for volatile reads. genrules using $(date), $(git rev-parse HEAD), etc.
grep -r 'date\|git rev\|uname\|hostname' $(bazel info workspace)/BUILD* || true
Step 4: Check for environment variables leaking into actions
bazel aquery '//your/package:target' --output=json | \
python3 -c "import json,sys; data=json.load(sys.stdin); \
[print(a.get('environmentVariables','')) for a in data.get('actions',[])]"
Paste the diff output directly into a Claude Code session and ask it to explain which declared input changed and why. Claude is particularly good at identifying when a genrule embeds a timestamp or when a glob() pattern accidentally picks up .pyc or __pycache__ files that differ between machines.
Common root causes and fixes:
| Root Cause | Symptom | Fix |
|---|---|---|
stamp = True in cc_binary |
Always misses on fresh checkout | Set stamp = 0 or use --nostamp |
glob picks up generated files |
Misses after any build | Add exclusion patterns to glob() |
Env var in genrule cmd |
Misses on different machines | Remove env var or declare it in env |
| Different Bazel versions | Misses across CI and local | Pin .bazelversion |
| Non-hermetic toolchain | Misses on OS version changes | Use rules_cc hermetic toolchain |
ctx.actions.run_shell with date |
Always misses | Replace with deterministic equivalent |
Workflow 3: Optimizing Cache Usage
Claude Code can recommend optimizations based on your build patterns:
- Modularize targets for better granularity
- Use
cc_shared_libraryfor shared C++ dependencies - Configure fine-grained invalidation for generated files
Beyond those basics, there are several optimization patterns that Claude Code can help you implement systematically.
Split fat targets into smaller ones. A single cc_library that aggregates hundreds of source files means any change to any file invalidates the entire target. Breaking it up means only the changed sub-library needs rebuilding:
Before: one fat target
cc_library(
name = "all_utils",
srcs = glob(["/*.cc"]),
hdrs = glob(["/*.h"]),
)
After: fine-grained targets
cc_library(
name = "string_utils",
srcs = ["string_utils.cc"],
hdrs = ["string_utils.h"],
)
cc_library(
name = "file_utils",
srcs = ["file_utils.cc"],
hdrs = ["file_utils.h"],
deps = [":string_utils"],
)
Use exports_files to share headers without rebuilding. When many targets depend on the same header, having it as an explicit target prevents unnecessary rebuilds:
exports_files(["common_types.h"])
cc_library(
name = "my_lib",
hdrs = [":common_types.h"],
srcs = ["my_lib.cc"],
)
Instrument your CI pipeline to report hit rates over time. A drop in hit rate is often the first signal that a refactor introduced a non-hermetic dependency:
.github/workflows/build.yml (excerpt)
- name: Build with remote cache
run: |
bazel build //... \
--config=ci \
--build_event_json_file=build_events.json
- name: Report cache hit rate
run: python3 scripts/analyze_cache.py build_events.json >> $GITHUB_STEP_SUMMARY
Best Practices for Remote Cache Workflows
Authentication and Security
Always use authenticated connections to your remote cache, especially in production environments. Claude Code can help you set up credentials securely:
Store credentials in a secure location
export BAZEL_REMOTE_CACHE_KEY="$(cat ~/.bazel/cache-key)"
For CI environments, use short-lived tokens rather than static API keys where possible. Most managed cache providers support OIDC token exchange, which eliminates the need to store secrets at all:
GitHub Actions example: OIDC-based auth for BuildBuddy
- name: Authenticate to remote cache
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.SA_EMAIL }}
- name: Build
run: |
bazel build //... \
--config=ci \
--google_default_credentials
For self-hosted bazel-remote, restrict write access by IP range or require a client certificate. Cache poisoning. where a malicious or broken build writes incorrect artifacts. is a real risk in shared environments. A useful defensive .bazelrc pattern for local developers:
Developer machines: read-only access to the shared cache
build:dev --remote_upload_local_results=false
CI only: write access via a separate token stored in CI secrets
build:ci --remote_upload_local_results=true
build:ci --remote_cache_header=Authorization=Bearer ${CI_CACHE_WRITE_TOKEN}
Cache Invalidation Strategy
Sometimes you need to intentionally invalidate cache entries. Create a skill that handles this:
---
name: bazel-cache-invalidate
description: Safely invalidate Bazel cache entries
---
Cache Invalidation Helper
When you need to invalidate specific targets, use:
Invalidate specific targets
bazel clean --experimental_force_clean //target:to_invalidate
For complete cache reset (use carefully)
bazel clean --expunge
A more targeted approach avoids nuking the entire cache. Bazel does not provide a built-in “delete this key” command for remote caches, but you can force a re-upload by slightly modifying the action’s declared inputs or environment. The cleanest production pattern is to use a cache namespace (sometimes called a “cache silo”):
Rotate the cache namespace to force a clean slate for all users
build --remote_default_exec_properties=cache-silo=2026-03-15
Changing the cache-silo value effectively invalidates every cached artifact without deleting anything from the storage backend. Old entries will expire via LRU eviction.
Monitoring and Alerts
Integrate cache monitoring into your CI/CD pipeline:
build:ci:
# Run with remote cache
build --remote_cache=$CACHE_URL
# Report cache statistics
build --build_event_json_file=build_events.json
Then parse the JSON to extract cache hit rates and alert on degradation.
A complete GitHub Actions monitoring job that posts a summary and fails if the hit rate drops below a threshold:
- name: Check cache health
run: |
python3 - <<'EOF'
import json, sys
from pathlib import Path
events_path = Path("build_events.json")
if not events_path.exists():
print("No build_events.json found, skipping cache health check")
sys.exit(0)
hits = misses = 0
with open(events_path) as f:
for line in f:
try:
e = json.loads(line)
except Exception:
continue
action = e.get("action", {})
if not action:
continue
if action.get("cached") or action.get("cacheHit"):
hits += 1
else:
misses += 1
total = hits + misses
rate = (hits / total * 100) if total else 0
print(f"Cache hit rate: {rate:.1f}% ({hits}/{total})")
THRESHOLD = 60
if total > 50 and rate < THRESHOLD:
print(f"ALERT: Cache hit rate {rate:.1f}% is below threshold {THRESHOLD}%")
sys.exit(1)
EOF
Setting up a Datadog or Grafana dashboard for cache hit rates over time gives you the long-term visibility you need to catch regressions before they affect developer productivity.
Troubleshooting Common Issues
Build Fails When Remote Cache Is Unavailable
By default Bazel treats a remote cache failure as a build failure. Add --remote_local_fallback to fall back to local execution gracefully:
build:remote --remote_local_fallback
build:remote --remote_local_fallback_strategy=local
TLS Certificate Errors
Self-signed certificates are a common problem. You can provide a custom CA cert:
build:remote --tls_client_certificate=/path/to/client.crt
build:remote --tls_client_key=/path/to/client.key
build:remote --remote_cache=grpcs://cache.internal:443
Or, for development only (never production), disable TLS verification:
build:dev-insecure --remote_cache=grpc://cache.internal:9090
Cache Writes Timing Out in CI
Large artifacts can time out on slow upload links. Increase the timeout and limit parallelism:
build:ci --remote_timeout=120s
build:ci --jobs=8
The --jobs flag controls how many concurrent Bazel actions run, which indirectly caps the number of simultaneous cache uploads.
Conclusion
Integrating Claude Code with Bazel remote caching creates a powerful workflow for build optimization. By automating cache management tasks, debugging issues, and providing actionable insights, Claude Code helps your team achieve faster builds and better developer experience. Start with a simple cache configuration, use skills to manage common operations, and progressively optimize as your build patterns mature.
The key is starting simple, configure a basic remote cache, verify it works, then layer on Claude Code skills to handle the operational complexities. Use the cache hit rate analysis scripts to identify your worst-performing targets, consult Claude Code to understand why those targets miss, and apply the targeted fixes from the troubleshooting table. Your team will thank you when those build times drop from minutes to seconds, and your CI queue times shrink to match.
Try it: Paste your error into our Error Diagnostic for an instant fix.
Related Reading
- Claude Code for Bazel Build System Workflow Guide
- Claude Code for Prometheus Remote Write Workflow
- Claude Code Turborepo Remote Caching Setup Workflow Guide
- Claude Code for Docs as Code Workflow Tutorial Guide
- Claude Code for Knowledge Sharing Workflow Tutorial
- Claude Code for HAProxy Load Balancer Workflow
- Claude Code for Retool Internal Tools Workflow
- Claude Code for Android DataStore Workflow Guide
- Claude Code Literature Review Summarization Workflow
- Claude Code for OpenTelemetry Metrics Workflow Guide
- Claude Code for Halmos Symbolic Workflow Guide
Built by theluckystrike. More at zovo.one
Get started → Generate your project setup with our Project Starter.