A high-performance HTTP reverse proxy with native Range request caching. Built from the ground up for streaming media, it downloads content from upstream in parallel chunks and caches everything on disk — including byte ranges.
- Native Range Caching: Unlike Varnish (which passthroughs Range requests), varc caches byte ranges on disk and serves them from cache. Concurrent range requests are coalesced into a single upstream fetch.
- Parallel Chunked Downloading: Downloads files in parallel streams for maximum throughput on high-latency connections.
- Disk-Backed Cache: Sparse file support, hash-verified metadata, configurable max age/size with background eviction.
- Cache Purge: Send
PURGErequests to evict specific URLs from cache immediately. - Stale-Serve on Error: When upstream is unreachable, varc serves stale cached content instead of returning 5xx.
- Conditional Requests:
If-Modified-SinceandIf-None-Matchare respected — returns 304 when content hasn't changed. - Smart Passthrough: POST, PUT, PATCH, DELETE, and requests with Authorization or Cookie headers bypass the cache and proxy directly upstream.
- Built-in Metrics: Track hit/miss ratio, bytes served, purge count, and cache size via a live JSON endpoint.
- Multiple Access Modes: Standalone HTTP server, Caddy module, or Go library.
- Flexible Cache Keys: Optional query parameter stripping, domain stripping, hash sharding.
- Caddy Ready: Native Caddy module for easy integration.
- Docker Ready: Minimal Alpine-based Docker image.
- Docker or Go 1.25+
You can run varc directly using Docker:
docker run -d \
-p 8080:8080 \
-v /path/to/host/cache:/tmp/varc_cache \
ghcr.io/tgdrive/varc --cache-dir /tmp/varc_cache# Stream a file via query parameter
curl "http://localhost:8080/stream?url=https://example.com/video.mp4"
# Or Base64-encoded path
curl "http://localhost:8080/stream/aHR0cHM6Ly9leGFtcGxlLmNvbS92aWRlby5tcDQ"
# Range requests are cached natively
curl -H "Range: bytes=0-999999" "http://localhost:8080/stream?url=https://example.com/video.mp4"
| Flag | Default | Description |
|---|---|---|
--port |
8080 |
Port to listen on |
--cache-dir |
$TMPDIR/varc_cache |
Cache directory on local disk |
--chunk-size |
128M |
Chunk size for parallel downloads; accepts suffixes (K, M, G, T) |
--chunk-streams |
2 |
Number of parallel download streams |
--max-age |
1h |
Maximum cache age (Go duration format) |
--max-size |
unlimited | Maximum cache size (e.g., 10G); disables eviction when unset |
--strip-query |
false |
Strip query params from URL before hashing cache key |
--strip-domain |
false |
Strip domain from URL before hashing cache key (shared cache for any origin) |
--shard-level |
1 |
Hash shard depth for cache paths (0 = flat, 3 = ab/cd/ef/hash) |
# Static upstream (proxy prefix)
example.com {
varc https://upstream.example.com {
cache_dir /data/cache
chunk_size 4M
chunk_streams 4
max_age 24h
max_size 50G
strip_query
shard_level 3
passthrough
}
reverse_proxy localhost:8080
}# Dynamic upstream (resolve from request query/base64 path — no upstream prefix needed)
example.com {
varc {
cache_dir /data/cache
metrics /varc/stats
}
reverse_proxy localhost:8080
}| Subdirective | Default | Description |
|---|---|---|
| positional | "" |
Upstream URL (optional, first argument; can also use upstream subdirective) |
upstream |
"" |
Upstream URL via named subdirective (alternative to positional arg) |
passthrough |
false |
Enable cache bypass (POST/auth/cookie) + call next handler on cache miss |
metrics |
"" |
Path to serve JSON metrics (e.g., /varc/stats) |
cache_dir |
$TMPDIR/varc_cache |
Cache directory on local disk |
chunk_size |
128M |
Chunk size for parallel downloads (accepts K, M, G, T suffixes) |
chunk_streams |
2 |
Number of parallel download streams |
max_age |
1h |
Maximum cache age (Go duration: 24h, 7d not supported — use 168h) |
max_size |
unlimited | Maximum cache size (e.g., 10G); oldest entries evicted first |
strip_query |
false |
Boolean flag — omit value to enable |
strip_domain |
false |
Boolean flag — omit value to enable |
shard_level |
1 |
Hash shard depth (0 = flat directory, N = N levels of 2-char subdirectories) |
When no upstream is configured, varc resolves the target URL from the request:
- Query param:
?url=https://example.com/file(same as standalone mode) - Base64 path:
/stream/<base64-encoded-url>(same as standalone mode)
This is useful for caching arbitrary URLs at a single entry point.
Build a custom Caddy binary with the varc module baked in using xcaddy:
# Install xcaddy
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
# Build Caddy + varc
xcaddy build --with github.com/tgdrive/varc/caddy
# Verify the module is registered
./caddy list-modules | grep varc
# → http.handlers.varcBy default xcaddy pulls the latest tagged release. To pin a specific version:
xcaddy build --with github.com/tgdrive/varc/caddy@v1.2.3To work with a local checkout during development:
xcaddy build --with github.com/tgdrive/varc/caddy=/absolute/path/to/varcFor production, build a custom Caddy image with varc:
FROM caddy:builder AS builder
RUN xcaddy build \
--with github.com/tgdrive/varc/caddy=github.com/tgdrive/varc
FROM caddy:latest
COPY --from=builder /usr/bin/caddy /usr/bin/caddyOr use a single-stage build:
FROM caddy:builder
RUN xcaddy build \
--with github.com/tgdrive/varc/caddy=github.com/tgdrive/varcOnce built, use the varc directive in your Caddyfile:
example.com {
varc https://upstream.example.com {
cache_dir /data/caddy/cache/varc
chunk_size 4M
chunk_streams 4
max_age 24h
max_size 50G
strip_query
passthrough
metrics /varc/stats
}
reverse_proxy localhost:8080
}- Request arrives → proxy resolves the upstream URL via query param or base64 path.
- Passthrough check → POST/PUT/PATCH/DELETE and requests with
AuthorizationorCookieheaders skip the cache and are proxied directly to upstream. - Cache check → if the file is already cached on disk and not stale, serve directly from cache (with
ETagandLast-Modifiedfor conditional validation). - Conditional validation →
If-Modified-SinceandIf-None-Matchare checked against the cached entry; returns 304 if content is unchanged. - Cache miss → file is downloaded from upstream in parallel chunks using Range requests, written to disk cache, and streamed to the client.
- Range requests → if the requested range is partially cached, only the missing bytes are fetched from upstream. Fully cached ranges are served without touching the upstream.
- Error fallback → if the upstream fetch fails and stale data exists in cache, the stale data is served with an
X-Cache: STALEheader. - Cache cleanup → background cleaner evicts expired or oversized entries.
Evict a specific URL from the cache immediately by sending a PURGE request:
curl -X PURGE "http://localhost:8080/stream?url=https://example.com/video.mp4"This removes both the cached file and the internal URL mapping. The next request for the same URL will be a full cache miss.
Varc exposes cache performance metrics as a JSON snapshot via the Go library:
stats := handler.Metrics().Snapshot()
// map[string]int64{
// "requests": 42,
// "hits": 30,
// "misses": 12,
// "bytes_served": 1048576,
// "bytes_from_upstream": 2097152,
// "purges": 1,
// }Cache engine stats (items count, bytes used, upload queue depth) are merged into the same snapshot.
In the Caddy module, configure a metrics endpoint with the metrics subdirective:
varc https://upstream.example.com {
metrics /varc/stats
}Then curl http://localhost:8080/varc/stats returns the same JSON snapshot.
When the upstream server is unreachable or returns an error, varc automatically serves any cached data it has for the requested URL. Responses served from stale cache include an X-Cache: STALE header so clients can distinguish stale from fresh.
If a Logger is configured (via types.Logger), each request is logged with:
[proxy] GET /stream?url=https://example.com/video.mp4 200 1048576 1.234s
Format: [proxy] METHOD URL STATUS SIZE DURATION
In standalone mode, this uses zap structured logging (production quality).
- Parallel chunked download with configurable stream count
- Sparse file support — no wasted disk for uncached ranges
- Concurrent readers don't block each other
- Metadata is persisted as JSON alongside cached data
# Build
go build -o varc .
# Run tests
go test ./...
# Cross-compile
GOOS=linux GOARCH=amd64 go build -o varc-linux-amd64 .