Production-ready remote cache

Cut build times with a dedicated cache server.

A focused, high-performance remote build cache for Gradle and ccache. Implements the standard HTTP build cache protocol with pluggable storage, optional Basic authentication, built-in observability, and zero external dependencies beyond your chosen backend. Includes a hybrid local+S3 mode with configurable local-cache TTL eviction.

0 MB
Docker image
0
HTTP endpoints
0
Coordination overhead
bash
# Start serving cache entries locally
$ go run ./app -storage=local -dir=./cache

# Or point it at S3
$ go run ./app -storage=s3 \
  -s3-bucket=my-cache \
  -s3-region=us-east-1

# Or use hybrid: local cache backed by S3, with TTL eviction
$ go run ./app -storage=hybrid \
  -dir=./cache \
  -s3-bucket=my-cache \
  -s3-region=us-east-1 \
  -local-ttl=7d

Simple, focused, and stateless.

The server speaks plain HTTP. Gradle clients push and pull cache entries through a predictable REST surface. Storage is pluggable, metrics are automatic, and there is no coordination between replicas.

Build Client

Gradle or ccache: HEAD to check, GET to fetch, PUT to store. Per-project cache isolation via path prefixes.

HTTP/1.1

Cache Server

Go HTTP server with request instrumentation, path validation, and configurable upload limits.

Stateless

Storage Backend

Local filesystem, S3-compatible object store, or a hybrid tier that caches locally and backs everything with S3.

Pluggable

Three endpoints. Nothing more.

The server implements the standard HTTP build cache protocol used by Gradle and ccache. Each cache entry is addressed by a cacheID and an entryKey, forming the path /cache/{cacheID}/{entryKey}.

Endpoints

GET

/health

Liveness and readiness probe. Returns 200 with body "ok".

GET

/metrics

Prometheus exposition format. All counters, histograms, and gauges.

HEAD

/cache/{id}/{key}

Check existence and size. Emits hit/miss metrics per cache ID.

GET

/cache/{id}/{key}

Retrieve entry. Uses ServeContent for local files to support range requests. Streams S3 bodies directly.

PUT

/cache/{id}/{key}

Store entry. Rejects empty bodies and payloads exceeding the configured max upload size.

Path Security

The parser validates every incoming path before it reaches storage. Directory traversal is rejected at the edge.

// Rejected patterns
/cache/myid/../secret    // contains ".."
/cache/myid\secret       // contains backslash
/cache/myid/             // empty entry key
/cache//key              // empty cache ID

Storage keys are derived as cacheID/entryKey. No user input ever reaches the filesystem or S3 client unsanitized.

Swap backends without changing code.

All implementations satisfy the same three-method interface. Switch from local disk to S3, or to a hybrid tier that combines both, with a single flag change.

Local Filesystem

Fastest path for single-node or NFS setups

Atomic writes — data is written to a temp file and renamed into place. Partial writes are never visible to readers.
Concurrency-safe — read-write mutex protects metadata operations while allowing concurrent reads.
Seekable streams — local files expose io.ReadSeeker, so the server can use http.ServeContent for automatic range request support and correct Last-Modified headers.

S3-Compatible

AWS S3, MinIO, Wasabi, DigitalOcean Spaces

AWS SDK v2 — uses the standard credential chain: environment variables, shared credentials file, then IAM role.
MinIO ready — custom endpoints are supported with path-style URLs, so on-premise MinIO clusters work out of the box.
Key prefixes — optional S3_PREFIX lets you share a single bucket across multiple cache servers or environments.

Hybrid Local + S3

Best of both: speed of local, durability of S3

Local-first reads — serve cache entries from local disk. If missing, download from S3, store locally, then serve.
Dual writes — every PUT persists to both local storage and S3, so replicas and rebuilds can still fetch from S3.
Local TTL eviction — automatic daily cleanup removes local entries not accessed for the configured TTL. Mount with strictatime for accurate tracking.

Local TTL depends on access-time tracking

The cleanup job uses each file's last access time (atime). Many Linux systems mount volumes with relatime or noatime for performance. For accurate TTL eviction, mount your cache directory with strictatime (or add mountOptions: [strictatime] in Kubernetes).

Metrics that matter.

Every request is instrumented with latency histograms, per-cache-ID counters, and in-flight gauges. Scrape them with Prometheus or point Grafana directly at the metrics endpoint.

/metrics
# HELP gradle_cache_requests_total Total HTTP requests
# TYPE gradle_cache_requests_total counter
gradle_cache_requests_total{method="GET",handler="cache",status="200",cache_id="myapp"} 1842
gradle_cache_requests_total{method="PUT",handler="cache",status="201",cache_id="myapp"} 523

# HELP gradle_cache_request_duration_seconds Request latency
# TYPE gradle_cache_request_duration_seconds histogram
gradle_cache_request_duration_seconds_bucket{method="GET",le="0.1"} 1520
gradle_cache_request_duration_seconds_bucket{method="GET",le="0.5"} 1801
gradle_cache_request_duration_seconds_bucket{method="GET",le="+Inf"} 1842
gradle_cache_request_duration_seconds_sum 94.23
gradle_cache_request_duration_seconds_count 1842

# HELP gradle_cache_hits_total Cache hits
gradle_cache_hits_total{cache_id="myapp"} 1287
gradle_cache_misses_total{cache_id="myapp"} 555

# HELP gradle_cache_in_flight_requests Active requests
gradle_cache_in_flight_requests 12

Prometheus Native

All metrics follow the OpenMetrics exposition format. No adapters, no exporters, no sidecars. Just point your Prometheus scrape config at :8080/metrics and start querying.

RED Instrumentation

Request rate, errors, and duration are captured for every handler. A custom response writer records the exact status code before the connection closes, so your error budgets are always accurate.

Graceful Shutdown

On SIGINT or SIGTERM, the server stops accepting new connections and waits up to 10 seconds for in-flight requests to finish. No partial cache entries, no dropped uploads, no metric gaps.

Run it anywhere.

Single static binary, a 15 MB Docker image, a production Helm chart, or raw Kubernetes manifests with HPA and health probes.

bashterminal
make build

# Local filesystem
./go-http-cache-server -storage=local -dir=./cache-data

# S3
export STORAGE_TYPE=s3
export S3_BUCKET=my-gradle-cache
export S3_REGION=us-east-1
export S3_CONCURRENCY=15
./go-http-cache-server

# Hybrid: local cache backed by S3, with 7-day local TTL
export STORAGE_TYPE=hybrid
export LOCAL_DIR=./cache-data
export S3_BUCKET=my-gradle-cache
export S3_REGION=us-east-1
export LOCAL_TTL=7d
./go-http-cache-server

# With upload limit (10 MiB)
./go-http-cache-server -storage=local -dir=./cache -max-upload=10485760

# With HTTP Basic authentication
./go-http-cache-server -storage=local -dir=./cache \
  -auth-username=gradle \
  -auth-password=change-me
bashDockerfile
# Multi-stage Alpine build (~15 MB final image)
docker build -t go-http-cache-server .

# Non-root user, CA certs included for S3 TLS
docker run -p 8080:8080 \
  -e STORAGE_TYPE=s3 \
  -e S3_BUCKET=my-cache \
  -e S3_REGION=us-east-1 \
  -e S3_CONCURRENCY=15 \
  -e AUTH_USERNAME=gradle \
  -e AUTH_PASSWORD=change-me \
  -e AWS_ACCESS_KEY_ID=... \
  -e AWS_SECRET_ACCESS_KEY=... \
  go-http-cache-server

# Hybrid with local TTL: mount a host volume for the local cache
docker run -p 8080:8080 \
  -v $(pwd)/cache-data:/app/cache-data \
  -e STORAGE_TYPE=hybrid \
  -e LOCAL_DIR=/app/cache-data \
  -e LOCAL_TTL=7d \
  -e S3_BUCKET=my-cache \
  -e S3_REGION=us-east-1 \
  -e AWS_ACCESS_KEY_ID=... \
  -e AWS_SECRET_ACCESS_KEY=... \
  go-http-cache-server
bashhelm
# Install directly from the OCI registry
helm install gradle-cache oci://ghcr.io/akrumov/go-http-cache-server --version 0.1.0

# Install with a custom values file
helm install gradle-cache oci://ghcr.io/akrumov/go-http-cache-server \
  --version 0.1.0 -f my-values.yaml

# Upgrade after changing values
helm upgrade gradle-cache oci://ghcr.io/akrumov/go-http-cache-server \
  --version 0.1.0 -f my-values.yaml
yamlmy-values.yaml
config:
  storageType: s3
  s3Bucket: my-gradle-cache
  s3Region: us-east-1
  s3Endpoint: ""          # set for MinIO
  s3Concurrency: 15       # parallel S3 part uploads
  # localTTL and localCleanupInterval apply to local and hybrid storage
  localTTL: "7d"
  localCleanupInterval: "24h"

replicaCount: 2

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70

persistence:
  enabled: false          # enable for local storage with PVC
  size: 10Gi

resources:
  requests:
    memory: 2Gi
    cpu: 1000m
  limits:
    memory: 4Gi
    cpu: 2000m

secret:
  data:
    AWS_ACCESS_KEY_ID: ""
    AWS_SECRET_ACCESS_KEY: ""
    AUTH_USERNAME: "gradle"
    AUTH_PASSWORD: "change-me"

Artifact Hub

kotlinsettings.gradle.kts
// settings.gradle.kts
buildCache {
    local { isEnabled = true }
    remote<HttpBuildCache> {
        url = uri("http://localhost:8080/cache/myapp")
        isEnabled = true
        isPush = providers.environmentVariable("CI").isPresent
        credentials {
            username = providers.environmentVariable("GRADLE_CACHE_USERNAME").orElse("").get()
            password = providers.environmentVariable("GRADLE_CACHE_PASSWORD").orElse("").get()
        }
    }
}

// gradle.properties
org.gradle.caching=true
iniccache.conf
# ccache.conf
remote_storage = http|url=http://localhost:8080/cache/ccache

# With HTTP Basic authentication:
# remote_storage = http|url=http://localhost:8080/cache/ccache|credentials=gradle:change-me

# Or set via environment variable:
# export CCACHE_REMOTE_STORAGE="http|url=http://localhost:8080/cache/ccache"

Basic authentication

Set a username and password to protect cache reads, writes, and metrics with HTTP Basic authentication. Health probes stay open.

Zero-downtime updates

Helm upgrades and raw-manifest rolling updates both ensure maxUnavailable stays zero. New pods pass readiness before old ones terminate.

Resource-conscious

Production defaults are 2 GiB memory request and 1 CPU, fully tunable through Helm values. Concurrency-aware for high-throughput S3 workloads.

Start caching smarter.

Open source under the MIT License. Contributions welcome.