Small .NET backend service for resilient API traffic.
cacheguard is a portfolio project around production-style API behavior:
rate limiting, request coalescing, circuit breaking, response caching, and
stale fallback when an upstream service fails.
It is intentionally not a CRUD app. The project shows how a backend service can protect an origin API, reduce duplicate work, and keep returning useful data when dependencies are temporarily unhealthy.
- per-client token-bucket rate limiting
- response caching with separate fresh and stale windows
- circuit breaker with closed, open, and half-open states
- request coalescing for concurrent identical GET requests
- stale-if-error behavior when an origin returns
5xx - a built-in demo origin so the project is testable without external services
- unit tests for the core resilience policies
- GitHub Actions and Docker packaging
dotnet test
dotnet run --project src/CacheGuard.Api --urls http://localhost:18081In another terminal:
curl -i "http://localhost:18081/proxy/catalog/42?value=first"
curl -i "http://localhost:18081/proxy/catalog/42?value=first"The first request should return x-cache: MISS; the second should return
x-cache: HIT.
Warm the cache:
curl -i "http://localhost:18081/proxy/catalog/42?value=stable"Wait longer than the default fresh cache TTL of 20 seconds, then simulate an origin failure for the same cache key:
curl -i "http://localhost:18081/proxy/catalog/42?value=stable&_cg_status=503"The response should include:
x-cache: STALE
x-cacheguard-warning: origin-failed-served-stale
_cg_status and _cg_delayMs are demo-only controls. They are passed to the
built-in origin but excluded from the cache key so failure modes can be tested
against the same cached resource.
The default token bucket allows 8 requests per client/path per 10 seconds:
for i in {1..10}; do curl -i "http://localhost:18081/proxy/rate-demo" ; doneLater responses should return 429 with a Retry-After header.
Trigger repeated origin failures:
curl -i "http://localhost:18081/proxy/unhealthy?_cg_status=503"
curl -i "http://localhost:18081/proxy/unhealthy?_cg_status=503"
curl -i "http://localhost:18081/proxy/unhealthy?_cg_status=503"
curl -i "http://localhost:18081/proxy/unhealthy?_cg_status=503"After the threshold is reached, the service returns 503 with
{"error":"circuit_open"} until the open window expires.
curl -s http://localhost:18081/admin/snapshotThe snapshot exposes counters for origin calls, cache hits, stale hits, coalesced requests, rate-limited requests, circuit-open requests, and demo origin requests.
| Variable | Default | Meaning |
|---|---|---|
UPSTREAM_BASE_URL |
built-in demo origin | Real upstream API base URL |
CACHEGUARD_RATE_LIMIT_CAPACITY |
8 |
Requests allowed per client/path window |
CACHEGUARD_RATE_LIMIT_WINDOW_SECONDS |
10 |
Token bucket refill window |
CACHEGUARD_CACHE_TTL_SECONDS |
20 |
Fresh cache duration |
CACHEGUARD_STALE_TTL_SECONDS |
120 |
Stale fallback duration after freshness expires |
CACHEGUARD_CIRCUIT_FAILURE_THRESHOLD |
3 |
Consecutive origin failures before opening |
CACHEGUARD_CIRCUIT_OPEN_SECONDS |
15 |
Open circuit duration before half-open probe |
.NET 10, ASP.NET Core Minimal APIs, HttpClientFactory, xUnit, GitHub Actions,
Docker.