Git cache
The git cache feature stores frequently-cloned repository data locally, serving
subsequent git clone and git fetch requests from cache rather than
forwarding every request to GitHub. This reduces load on GitHub, improves clone
performance for agents, and decreases egress bandwidth.
How It Works
The cache operates at the Git smart HTTP protocol level (protocol v2):
-
ls-refsis always forwarded to GitHub. This serves as the access verification gate — GitHub checks the user's token and returns the repository's current refs. If GitHub rejects the request, no cached data is exposed. -
fetchis served from cache when possible. If all requested objects (wants) are present in the local cache, the packfile is generated locally using pure Go (nogitbinary required). On a cache miss, GHP fetches from upstream using the per-request credential and then serves from cache. When a GitHub App service token is configured, async cache warming keeps the cache hot so most requests are served as hits. -
Async cache warming. When
ls-refsdetects that upstream refs have changed since the last fetch, a background goroutine fetches the latest objects so subsequent requests benefit from the cache.
This design ensures that GitHub's access control is always enforced — a user who cannot access a repository via GitHub will never receive cached data.
Enabling the Cache
1. Enable in configuration
cache:
enabled: true
storage_path: "/var/lib/ghp/cache" # path for cached bare repos
Or via environment variables:
export GHP_CACHE_ENABLED=true
export GHP_CACHE_STORAGE_PATH=/var/lib/ghp/cache
2. Add repositories to cache
Use the admin UI or API to specify which repositories should be cached. Repositories not listed pass through to GitHub as normal.
Admin UI: Navigate to the admin panel and use the "Cached Repositories"
section to add repositories by owner and name. An optional Timeout field
controls how long the proxy waits for upstream GitHub responses (default: 30
seconds). Large repositories like torvalds/linux may need 600 seconds or more.
An optional Cache Size Limit (in MB) caps the total size of cached protocol
response files per repository; when exceeded, the oldest files are evicted first.
Both timeout and cache size limit can be edited on existing repos via the Edit
button in the table.
API:
# Add a repository to the cache list
curl -X POST https://ghp.example.com/api/cached-repos \
-H "Authorization: Bearer ghpr_..." \
-H "Content-Type: application/json" \
-d '{"owner": "myorg", "name": "myrepo"}'
# Add a large repository with a custom timeout (seconds, max 3600)
curl -X POST https://ghp.example.com/api/cached-repos \
-H "Authorization: Bearer ghpr_..." \
-H "Content-Type: application/json" \
-d '{"owner": "torvalds", "name": "linux", "timeout_seconds": 600}'
# Add a repository with a cache size limit (MB)
curl -X POST https://ghp.example.com/api/cached-repos \
-H "Authorization: Bearer ghpr_..." \
-H "Content-Type: application/json" \
-d '{"owner": "myorg", "name": "large-repo", "max_cache_size_mb": 500}'
# List cached repositories
curl https://ghp.example.com/api/cached-repos \
-H "Authorization: Bearer ghpr_..."
# Disable caching for a repository (keeps the record)
curl -X PATCH https://ghp.example.com/api/cached-repos/{id} \
-H "Authorization: Bearer ghpr_..." \
-H "Content-Type: application/json" \
-d '{"enabled": false}'
# Update the timeout for a repository
curl -X PATCH https://ghp.example.com/api/cached-repos/{id} \
-H "Authorization: Bearer ghpr_..." \
-H "Content-Type: application/json" \
-d '{"timeout_seconds": 300}'
# Set or update the cache size limit (MB); use 0 to clear (unlimited)
curl -X PATCH https://ghp.example.com/api/cached-repos/{id} \
-H "Authorization: Bearer ghpr_..." \
-H "Content-Type: application/json" \
-d '{"max_cache_size_mb": 200}'
# Remove a repository from the cache list
curl -X DELETE https://ghp.example.com/api/cached-repos/{id} \
-H "Authorization: Bearer ghpr_..."
Cache Size Limits & Cleanup
Each cached repository can have an optional max_cache_size_mb setting that
limits the total disk space used by cached protocol response files. When the
limit is exceeded, a background cleanup goroutine (running every 10 minutes)
evicts the oldest files (by last-access time) until the total is back under
the limit.
- No limit set (default): cached response files accumulate indefinitely.
Operators should monitor
ghp_cache_response_size_bytesand set limits on repositories that grow large. - Limit set: the cleanup goroutine enforces the cap automatically. Recently accessed cache entries are preserved (cache hits touch the file's mtime).
- Set via API:
max_cache_size_mbon POST or PATCH/api/cached-repos. - Set via UI: use the Cache Size Limit field when creating, or the Edit button on existing repositories.
Metrics
The cache exposes Prometheus metrics at the /metrics endpoint:
| Metric | Type | Labels | Description |
|---|---|---|---|
ghp_cache_fetch_total |
Counter | result |
Git fetch requests by result (hit, miss, rejected, error) |
ghp_cache_lsrefs_total |
Counter | — | ls-refs commands forwarded to upstream |
ghp_cache_warm_total |
Counter | result |
Cache warming operations by result (success, error) |
ghp_cache_repos_active |
Gauge | — | Number of repositories with caching enabled |
ghp_cache_request_total |
Counter | owner, repo, result |
Per-repository git requests with cache outcome |
ghp_cache_eviction_total |
Counter | owner, repo |
Protocol response files evicted by size-limit cleanup |
ghp_cache_response_size_bytes |
Gauge | owner, repo |
Current total size of cached protocol response files |
The ghp_proxy_decision_duration_seconds histogram includes a cache_lookup
stage measuring the time spent checking whether a request targets a cached
repository.
Identifying cache candidates
The ghp_cache_request_total metric tracks every git smart HTTP request with
per-repository labels and a result indicating the cache outcome: hit,
miss, nocache (not configured), bypass (configured but disabled),
error, rejected, or passthrough.
Use these queries to find repositories that would benefit from caching:
# Top uncached repos by request volume
topk(10, sum by (owner, repo) (ghp_cache_request_total{result="nocache"}))
# Cache hit rate per repo
sum by (owner, repo) (ghp_cache_request_total{result="hit"})
/ sum by (owner, repo) (ghp_cache_request_total)
See Monitoring for the full set of example queries.
Access Logs
When the cache feature is active, two additional fields appear in access log entries:
cache_state— the cache result for the request:hit,miss,rejected,error,passthrough, or empty for non-cached requestscache_repo— theowner/repoidentifier when the request targets a cache-enabled repository
Security Model
The cache maintains GitHub's access control invariant:
-
ls-refs is the authorization gate. Every session begins with an
ls-refscommand forwarded to GitHub using the user's own token. If GitHub returns an error (401, 403, 404), the session stops and no cached data is returned. -
fetch requires prior ls-refs. A
fetchcommand without a preceding successfulls-refsin the same request is rejected with an error. This prevents clients from directly requesting cached objects without proving access. -
Cached objects are content-addressed. Git objects are immutable and identified by a content hash (SHA-1 by default, or SHA-256 in SHA-256-mode repositories). A cache hit serves the same bytes GitHub would return.
Limitations
-
Protocol v2 only. The cache requires Git protocol v2 (
Git-Protocol: version=2header). Protocol v1 and dumb HTTP requests pass through to GitHub without caching. -
Read-only operations. Only
git-upload-pack(clone/fetch) is cached. Push operations (git-receive-pack) always go directly to GitHub. -
No push notification. The cache relies on
ls-refsto detect ref changes. If a push occurs between two fetch requests, the second fetch may trigger a cache miss and upstream fetch.
S3 Storage Backend
For horizontally-scaled deployments, the cache can store objects in an S3-compatible bucket instead of local filesystem:
cache:
enabled: true
s3_bucket: "ghp-cache"
s3_region: "us-east-1"
# s3_endpoint: "https://minio.internal:9000" # for non-AWS S3
When s3_bucket is set, storage_path is used as the key prefix within the
bucket. All ghp instances sharing the same bucket and prefix will share the
cache.
S3 backend is planned
The S3 storage backend is under development. Currently only local filesystem storage is available.