Case: High-Throughput Public API
Problem
Design a public REST API for a SaaS providing weather forecasts. Targets: 100K req/s peak, p99 < 100ms, 99.95% availability, multi-region.
Walkthrough
Clarify
- Read-heavy (99% reads, 1% writes from internal admin).
- Forecasts updated every 15 min.
- Cache TTL ~5 min OK (data freshness vs cost).
- Public; auth via API key + rate limit.
- Multi-region (US, EU, APAC).
- Cost-sensitive at this scale.
Capacity
100K req/s peak * 86400s ≈ 8.6B req/day
Storage: 200K cities * 1 KB forecast = 200 MB; small.
Compute: 100K rps / 1000 rps per instance = 100 instances per region.
Bandwidth: 100K * 5KB = 500 MB/s = 4 Gbps; CDN-served.
Architecture
[Client]
│ HTTPS
▼
[CDN — Cloudflare / Azure Front Door] ← caches public responses
│ cache miss
▼
[Regional Anycast → Container Apps]
│
├──→ [HybridCache: L1 memory + L2 Redis]
│ │ cache miss
│ ▼
└──→ [Read replica: Postgres or Cosmos]
Admin (1% writes):
[Admin] → [Write API → Master DB] → [event] → [Cache invalidation]
API design
GET /v1/weather/{city}
GET /v1/weather/{city}/hourly?days=3
GET /v1/weather/forecast?lat=&lon=
Headers:
Authorization: Bearer <api-key>
X-RateLimit-Remaining: 99
ETag: "abc"
Cache-Control: public, max-age=300
Errors: ProblemDetails
Caching layers
Browser → 5 min Cache-Control
CDN → 1 min edge cache
HybridCache L1 → 30 sec memory
HybridCache L2 → 5 min Redis
DB → ground truth
Cache hit at edge → no origin call. Massive cost saving.
Code structure
app.MapGet("/v1/weather/{city}",
async (string city, HybridCache cache, IWeatherRepo repo, CancellationToken ct) =>
{
var fc = await cache.GetOrCreateAsync(
$"weather:v1:{city}",
(state, ct) => state.repo.GetAsync(state.city, ct),
(city, repo),
new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(5) },
cancellationToken: ct);
return Results.Ok(fc, cacheControl: "public, max-age=300");
})
.CacheOutput(p => p.Expire(TimeSpan.FromMinutes(1)).VaryByValue(c => c.Request.Path))
.WithName("GetWeather");
builder.Services.AddRateLimiter(o =>
{
o.AddPolicy("api", c => RateLimitPartition.GetTokenBucketLimiter(
c.Request.Headers["X-API-Key"].ToString(),
_ => new TokenBucketRateLimiterOptions { TokenLimit = 100, ReplenishmentPeriod = TimeSpan.FromSeconds(1), TokensPerPeriod = 100 }));
});
app.UseRateLimiter();
Resilience
builder.Services.AddHttpClient<UpstreamWeatherProvider>()
.AddStandardResilienceHandler(o =>
{
o.AttemptTimeout = TimeSpan.FromSeconds(5);
o.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(15);
o.Retry.MaxRetryAttempts = 3;
o.CircuitBreaker.FailureRatio = 0.3;
});
Observability
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddAspNetCoreInstrumentation().AddHttpClientInstrumentation().AddOtlpExporter())
.WithMetrics(m => m.AddAspNetCoreInstrumentation().AddRuntimeInstrumentation().AddOtlpExporter());
Custom metrics: cache hit rate, upstream latency, RPS by API key.
Multi-region
- Active-active.
- Regional Postgres read replicas (or Cosmos with multi-region writes).
- Front Door for routing (latency-based).
- Replicate cache invalidations cross-region (Service Bus topic → regional consumers).
Cost optimization
- CDN absorbs 90%+ of traffic → only 10% hits origin.
- Cosmos vs Postgres cost depends on RPS profile; Postgres cheaper at this scale typically.
- Container Apps scale-to-zero NOT useful (constant traffic).
- Reserved instances; right-sized.
Failure modes
- DB outage → serve from cache (stale OK; alarm).
- Cache outage → fall through to DB; capacity hit; circuit breaker.
- Region outage → Front Door routes to healthy regions.
- Bad data: ETag invalidation; cache busting via version key (
weather:v2:{city}).
Trade-offs
| Choice | Why | Trade-off |
|---|---|---|
| Postgres + replicas | Cost-effective at scale | Regional write latency |
| HybridCache | L1+L2; stampede protection | Two layers complexity |
| 5-min cache TTL | Hits 90%+ | Stale up to 5 min |
| API key + rate limit | Simple; per-customer | Less granular than OAuth |
| CDN-fronted | Massive offload | Cache invalidation lag |
What we'd skip
- Kafka: overkill for this read-heavy use case.
- CQRS: read/write asymmetry not extreme.
- Saga: no multi-step workflow.
- Service mesh: a few services; YARP / direct LB enough.
What we'd add for higher scale
- Read replicas across more regions.
- Multiple Front Door tiers.
- Write throttling and queue for the admin write side.
- Per-tier (free, paid, premium) rate limits.
HTTP version
- HTTP/2 minimum.
- HTTP/3 (QUIC) for mobile / poor network.
- Both supported by ASP.NET Core 9 + Kestrel; CDN handles transparent fallback.
Senior interview signals
- Mentioning CDN before designing the API code.
- Right-sized DB (Postgres, not Kafka).
- Capacity math first.
- Failure modes explicitly.
- Trade-offs named.
- Cost considered.
- Skipping fancy tech we don't need.