Kura is a Rust server for building low-latency cache meshes for tenants, handling distributed cache traffic for binary artifacts and metadata.
Note
Kura comes from the Japanese word θ΅ (kura), which refers to a storehouse or warehouse. The name fits the system's role: keeping build artifacts and cache metadata stored durably and close at hand so they can be served with low latency.
- β‘ Hot reads come from local disk
- πͺ¨ Local metadata, multipart state, and the replication outbox live in RocksDB
- π Blobs and cache metadata replicate to peer nodes with eventual consistency
- π Nodes can discover peers through DNS and bootstrap themselves from already-running nodes
- π¦ The HTTP API covers key value entries, Xcode CAS artifacts, Gradle artifacts, multipart module uploads, Nx self-hosted cache artifacts, Metro cache artifacts, and namespace clean
- π§° The gRPC API exposes the Bazel Remote Execution cache services used by Bazel and Buck2
- π The local stack includes Grafana, Prometheus, Loki, Promtail, and Tempo traces
Run:
docker compose up --build -dUseful endpoints:
http://localhost:4101/uphttp://localhost:4102/uphttp://localhost:4103/upgrpc://localhost:5101for Bazel/Buck2 REAPI againstkura-usgrpc://localhost:5102for Bazel/Buck2 REAPI againstkura-eugrpc://localhost:5103for Bazel/Buck2 REAPI againstkura-aphttp://localhost:3000for Grafana withadmin/adminhttp://localhost:9090for Prometheushttp://localhost:3100for Lokihttp://localhost:3200for Tempo
Supported cache protocols:
BazelandBuck2: Bazel Remote Execution API v2 over gRPC onKURA_GRPC_PORTNx: self-hosted remote cache API onGET/PUT /v1/cache/{hash}React Native Metro:HttpStore/HttpGetStoreonGET/PUT /api/metro/cache/{cache_key}
Install Rust from mise.toml:
mise trust mise.toml
mise installRun tests:
mise x [email protected] -- cargo test
mise x [email protected] -- shellspecRuntime configuration is summarized in the table under Runtime Model And Limits. Kura now derives sensible defaults for the main FD, memory, and metadata-store budgets at startup when you do not set them explicitly.
Kura is easier to read by subsystem than by tutorial step. The sections below group the project by the main areas you operate or extend.
- π Protocol surfaces
- ποΈ Storage and replication
- βοΈ Runtime model and limits
- π Observability
- π£ Runtime analytics
- βΈοΈ Deployment options
- π§© Extensions and policy
Kura exposes multiple cache protocols behind one service:
The following HTTP surfaces are Tuist-specific client protocols. They exist so Tuist clients can talk to Kura without changing their cache behavior:
- π
Xcode CAS:POST/GET /api/cache/cas/{id}?tenant_id=...&namespace_id=... - ποΈ
Keyvalue / action-cache style entries:PUT /api/cache/keyvalue?tenant_id=...&namespace_id=... - π
Gradle:PUT/GET /api/cache/gradle/{cache_key}?tenant_id=...&namespace_id=... - π¦
Multipart module cache uploads:POST /api/cache/module/start?...,POST /api/cache/module/part?...,POST /api/cache/module/complete?...,HEAD/GET /api/cache/module/{id}?...
Kura also exposes broader ecosystem protocols that are not specific to Tuist:
- π§±
Nx:PUT/GET /v1/cache/{hash} - π±
Metro:PUT/GET /api/metro/cache/{cache_key} - π οΈ
BazelandBuck2: REAPI over gRPC onKURA_GRPC_PORT
The local compose stack is still the quickest way to exercise all of those surfaces together:
docker compose up --build -dExample Xcode artifact round trip:
curl -X POST \
"http://localhost:4101/api/cache/cas/artifact-1?tenant_id=acme&namespace_id=ios" \
-H "content-type: application/octet-stream" \
--data-binary "xcode-binary"
curl \
"http://localhost:4102/api/cache/cas/artifact-1?tenant_id=acme&namespace_id=ios"Example keyvalue entry round trip:
curl -X PUT \
"http://localhost:4101/api/cache/keyvalue?tenant_id=acme&namespace_id=ios" \
-H "content-type: application/json" \
-d '{"cas_id":"cas-1","entries":[{"value":"hello"},{"value":"world"}]}'
curl \
"http://localhost:4103/api/cache/keyvalue/cas-1?tenant_id=acme&namespace_id=ios"Kura splits storage into two planes:
- πͺ¨ RocksDB stores metadata, keyvalue payloads, multipart state, tombstones, segment lifecycle state, and the replication outbox.
- π¦ Segment files store large immutable binary artifacts for the hot path.
Replication is leaderless and eventually consistent:
- π local writes become durable together with their outbox work
- π peers bootstrap by pulling manifests, tombstones, and artifact bodies
- π DNS discovery can expand the peer set automatically
- π§ the outbox is processed incrementally so queue depth does not blow up heap usage during backlog
Peer-to-peer mTLS is available for the internal plane:
KURA_INTERNAL_PORTKURA_INTERNAL_TLS_CA_CERT_PATHKURA_INTERNAL_TLS_CERT_PATHKURA_INTERNAL_TLS_KEY_PATH
When peer mTLS is enabled:
- π
KURA_NODE_URLand every value inKURA_PEERSmust usehttps://...:<KURA_INTERNAL_PORT> - π the public API still stays on
KURA_PORT - π§±
/_internal/*is only served on the internal mTLS listener - πͺͺ the certificate configured through
KURA_INTERNAL_TLS_CERT_PATHshould be valid for both server and client auth - π·οΈ the certificate SANs must cover the hostname used in
KURA_NODE_URL
Kura is designed around explicit resource budgets instead of relying on ambient process limits.
When Optional is Yes, the Default column shows what Kura uses today. auto means Kura derives the value at startup from detected file-descriptor limits, memory limits, or CPU count.
| Name | Description | Optional | Default |
|---|---|---|---|
KURA_PORT |
Public HTTP port. | No | β |
KURA_GRPC_PORT |
gRPC port for REAPI. | No | β |
KURA_TENANT_ID |
Default tenant identifier for the node. | No | β |
KURA_REGION |
Region label advertised in metrics and replication state. | No | β |
KURA_TMP_DIR |
Temporary directory for staged request bodies and multipart assembly. | No | β |
KURA_DATA_DIR |
Persistent directory for metadata state and segment files. | No | β |
KURA_NODE_URL |
Canonical URL other peers use to reach this node. | No | β |
KURA_PEERS |
Seed peer list used before discovery converges. | Yes | KURA_NODE_URL |
KURA_DISCOVERY_DNS_NAME |
DNS name to probe for automatic peer discovery. | Yes | disabled |
KURA_FILE_DESCRIPTOR_POOL_SIZE |
App-managed file-descriptor budget for request and background I/O. | Yes | auto |
KURA_FILE_DESCRIPTOR_ACQUIRE_TIMEOUT_MS |
How long a request waits before FD backpressure fails the checkout. | Yes | 5000 |
KURA_SEGMENT_HANDLE_CACHE_SIZE |
Maximum number of pinned segment read handles; must stay below the FD pool size. | Yes | auto |
KURA_MEMORY_SOFT_LIMIT_BYTES |
Soft watermark where Kura starts shedding optional memory use. | Yes | auto |
KURA_MEMORY_HARD_LIMIT_BYTES |
Hard watermark where Kura pauses replication work and trims hot caches aggressively. | Yes | auto |
KURA_MANIFEST_CACHE_MAX_BYTES |
Maximum size of the in-memory manifest hot cache. | Yes | auto |
KURA_MAX_KEYVALUE_BYTES |
Maximum per-request keyvalue payload size on public and replication APIs. | Yes | 1048576 |
KURA_METADATA_STORE_MAX_OPEN_FILES |
Descriptor budget reserved for the metadata store itself. | Yes | auto |
KURA_METADATA_STORE_MAX_BACKGROUND_JOBS |
Background flush and compaction concurrency for the metadata store. | Yes | auto |
KURA_METADATA_STORE_READ_CACHE_BYTES |
Capacity of the metadata-store read cache. | Yes | auto |
KURA_METADATA_STORE_WRITE_BUFFER_POOL_BYTES |
Total memory budget reserved for metadata write buffering. | Yes | auto |
KURA_METADATA_STORE_WRITE_BUFFER_BYTES |
Size of each metadata write buffer before flush. | Yes | auto |
KURA_METADATA_STORE_MAX_WRITE_BUFFERS |
Maximum number of metadata write buffers kept in memory. | Yes | auto |
Auto-derived defaults currently follow these rules:
file_descriptor_limitcomes fromRLIMIT_NOFILEwhen available, otherwise Kura falls back to a conservative host default.memory_limit_bytescomes from the cgroup memory limit when available, otherwise Kura falls back to physical host memory.cpu_countcomes from detected parallelism via the runtime.KURA_FILE_DESCRIPTOR_POOL_SIZEisusable_fds / 8, clamped to[64, 256], whereusable_fdsis the detected FD limit minus reserved headroom.KURA_SEGMENT_HANDLE_CACHE_SIZEisKURA_FILE_DESCRIPTOR_POOL_SIZE / 4, clamped to[16, 64], and then capped below the FD pool so transient work keeps headroom.KURA_MEMORY_SOFT_LIMIT_BYTESis70%of detected memory, rounded down to MiB boundaries, with a minimum of128 MiB.KURA_MEMORY_HARD_LIMIT_BYTESis85%of detected memory, rounded down to MiB boundaries, and always at least64 MiBabove the soft limit.KURA_MANIFEST_CACHE_MAX_BYTESisKURA_MEMORY_SOFT_LIMIT_BYTES / 16, rounded down to MiB boundaries and clamped to[8 MiB, 64 MiB].KURA_METADATA_STORE_MAX_OPEN_FILESisusable_fds / 2, clamped to[128, 1024].KURA_METADATA_STORE_MAX_BACKGROUND_JOBSiscpu_count, clamped to[1, 8].KURA_METADATA_STORE_READ_CACHE_BYTESismemory_limit_bytes / 32, rounded down to MiB boundaries and clamped to[16 MiB, 128 MiB].KURA_METADATA_STORE_WRITE_BUFFER_POOL_BYTESfollows the samememory_limit_bytes / 32rule as the metadata-store read cache.KURA_METADATA_STORE_WRITE_BUFFER_BYTESisKURA_METADATA_STORE_WRITE_BUFFER_POOL_BYTES / 4, rounded down to MiB boundaries and clamped to[4 MiB, 32 MiB].KURA_METADATA_STORE_MAX_WRITE_BUFFERSisKURA_METADATA_STORE_WRITE_BUFFER_POOL_BYTES / KURA_METADATA_STORE_WRITE_BUFFER_BYTES, clamped to[2, 8].KURA_MAX_KEYVALUE_BYTESdefaults to1048576, andKURA_FILE_DESCRIPTOR_ACQUIRE_TIMEOUT_MSdefaults to5000.
A minimal direct-binary deployment still looks like:
KURA_PORT=4000 \
KURA_GRPC_PORT=50051 \
KURA_TENANT_ID=default \
KURA_REGION=eu-central \
KURA_TMP_DIR=/tmp/kura \
KURA_DATA_DIR=/var/cache/kura \
KURA_NODE_URL=http://cache-1.internal:4000 \
KURA_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://otel-collector:4318/v1/traces \
KURA_OTEL_SERVICE_NAME=kura-eu-central \
KURA_OTEL_DEPLOYMENT_ENVIRONMENT=production \
./target/release/kuraKura ships with a fairly complete local observability story:
- π Prometheus metrics
- π Grafana dashboards
- πͺ΅ Loki and Promtail logs
- π§ Tempo traces
Prometheus exposes live metadata-store memory gauges:
kura_rocksdb_block_cache_usage_byteskura_rocksdb_block_cache_pinned_usage_byteskura_rocksdb_block_cache_capacity_byteskura_rocksdb_write_buffer_usage_byteskura_rocksdb_write_buffer_capacity_bytes
Kura also exports:
- π¦ artifact read and write counters by
kind,client,artifact_class, andresult - π replication latency and result metrics
- πΎ file descriptor pool pressure metrics
- π§ manifest cache occupancy and admission metrics
Analytics webhooks are a separate optional subsystem that mirrors the older Tuist cache contract for Xcode and Gradle traffic.
When enabled:
- π Xcode upload and download events are sent to
/webhooks/cache - π Gradle upload and download events are sent to
/webhooks/gradle-cache - βοΈ requests are signed with
x-cache-signature - π§ requests also include
x-cache-endpoint - πͺΆ delivery stays in-memory and best-effort, so analytics never block the hot path
- π§― a per-pipeline circuit breaker opens after repeated delivery failures so Kura sheds analytics instead of backing up under a misbehaving upstream
Configure it with:
KURA_ANALYTICS_SERVER_URLKURA_ANALYTICS_SIGNING_KEY- optional
KURA_ANALYTICS_BATCH_SIZEdefault100 - optional
KURA_ANALYTICS_BATCH_TIMEOUT_MSdefault5000 - optional
KURA_ANALYTICS_QUEUE_CAPACITYdefault1000 - optional
KURA_ANALYTICS_REQUEST_TIMEOUT_MSdefault5000 - optional
KURA_ANALYTICS_CIRCUIT_BREAKER_FAILURE_THRESHOLDdefault5 - optional
KURA_ANALYTICS_CIRCUIT_BREAKER_OPEN_MSdefault30000
It also exposes analytics-specific runtime metrics for:
- π£ queue depth and drops
- π¦ batch sizes and flush outcomes
- π§― circuit-breaker state and open events
The repository includes a Helm chart at ops/helm/kura that deploys Kura as a StatefulSet with:
- πΎ one PVC per pod for metadata-state and segment storage
- π§ a headless service for stable pod DNS and peer discovery
- π a regular service exposing both HTTP and gRPC
- πͺ optional ingress for the HTTP API
- π§© optional inline extension script mounting through a
ConfigMap - π optional peer mTLS for
/_internal/*traffic via a mounted KubernetesSecret
Lint and render the chart:
helm lint ops/helm/kura
helm template kura ops/helm/kura --namespace kuraInstall it on a generic cluster:
helm upgrade --install kura ./ops/helm/kura \
--namespace kura \
--create-namespace \
--set image.repository=ghcr.io/tuist/kura \
--set image.tag=latest \
--set config.region=fr-par \
--set config.telemetry.otlpTracesEndpoint=http://otel-collector.monitoring.svc.cluster.local:4318/v1/tracesFor a local kind smoke test, the repo includes:
./test/e2e/kura_helm_kind.shTo enable peer mTLS in Kubernetes, set:
peerTls.enabled=truepeerTls.internalPort=<port>peerTls.secretName=<secret-with-ca-cert-and-key-material>
The referenced secret should contain the files configured by:
peerTls.caCertFileNamepeerTls.certFileNamepeerTls.keyFileName
When enabled, the chart advertises peer URLs over https on the internal port and mounts the secret into /etc/kura/peer-tls.
For Scaleway, start from the bundled overrides in ops/helm/kura/values-scaleway.yaml:
helm upgrade --install kura ./ops/helm/kura \
--namespace kura \
--create-namespace \
-f ./ops/helm/kura/values-scaleway.yaml \
--set image.repository=ghcr.io/tuist/kura \
--set image.tag=latest \
--set config.region=fr-par \
--set config.telemetry.otlpTracesEndpoint=http://otel-collector.monitoring.svc.cluster.local:4318/v1/tracesThat values file does two important things:
- πͺ uses a
LoadBalancerservice, which is the simplest way to expose Kura on Kapsule - πΎ pins persistence to
scw-bssd, which Scaleway documents as the default block storage class for Kapsule multi-AZ clusters
Kura can load one operator-provided extension script at startup to customize authentication, authorization, and response headers without recompiling the binary.
Core env vars:
KURA_EXTENSION_ENABLED=trueKURA_EXTENSION_SCRIPT_PATH=/etc/kura/extensions/hooks.luaKURA_EXTENSION_HOOK_TIMEOUT_MS=25KURA_EXTENSION_AUTH_CACHE_ALLOW_TTL_SECONDS=600KURA_EXTENSION_AUTH_CACHE_DENY_TTL_SECONDS=3KURA_EXTENSION_FAIL_CLOSED_AUTHENTICATE=trueKURA_EXTENSION_FAIL_CLOSED_AUTHORIZE=trueKURA_EXTENSION_FAIL_OPEN_RESPONSE_HEADERS=true
Generic host resources are also env-driven:
- βοΈ signers:
KURA_EXTENSION_SIGNER_<ID>_ALGORITHMKURA_EXTENSION_SIGNER_<ID>_SECRET
- πͺͺ JWT verifiers:
KURA_EXTENSION_JWT_VERIFIER_<ID>_ALGORITHMKURA_EXTENSION_JWT_VERIFIER_<ID>_SECRETKURA_EXTENSION_JWT_VERIFIER_<ID>_ISSUERKURA_EXTENSION_JWT_VERIFIER_<ID>_AUDIENCES
- π HTTP clients:
KURA_EXTENSION_HTTP_CLIENT_<ID>_BASE_URLKURA_EXTENSION_HTTP_CLIENT_<ID>_CONNECT_TIMEOUT_MSKURA_EXTENSION_HTTP_CLIENT_<ID>_REQUEST_TIMEOUT_MS
The script may define these hooks:
authenticate(ctx)authorize(ctx, principal)response_headers(ctx, principal)
The runtime keeps decision caching, metrics, timeouts, and cryptographic primitives in Rust, while the script supplies policy.
