Edge, Security & Multi-Region Deployment
CDN/WAF, rate limiting, abuse defense and geo-deployment for low latency
The edge stack: CDN, WAF, TLS, Anycast
After this you can lay out the layers a request passes through before your cluster.
Before a request ever touches your EKS cluster, it crosses four or five edge layers. In a Cursor system-design round, the candidates who lose are the ones who jump straight to pods and ingress and skip everything that happens at the planet's surface.
Cursor serves 1M+ daily active users from a desktop app that talks to backends for indexing, retrieval and model inference. Most of that traffic is latency-sensitive and a lot of it is expensive to serve. The edge is where you decide what gets cached, what gets blocked and which region a user lands in - before any of it costs you compute.
Walk the path a request takes, in order. Naming the layers buys you credibility before you've designed anything.
- 1DNS resolution. The client resolves your hostname. With Anycast and geo-aware DNS, the answer it gets already steers toward a nearby edge POP.
- 2Anycast / edge POP. The same IP is announced from many locations; BGP routes the user to the closest one. TCP and TLS handshakes terminate here, near the user, not in
us-east-1. - 3CDN cache check. If the response is cacheable and warm, the edge answers immediately and origin never sees the request.
- 4WAF + edge logic. Rules inspect the request for known attack classes; rate limiting and bot checks run here so junk traffic is shed early.
- 5Origin / regional load balancer. Only the surviving, uncacheable requests cross to a region's ALB and into the mesh.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Read top-down - the request enters at the client and only the survivors reach origin. Each layer sheds or protects so the one below it does less work.
What is and isn't cacheable for a developer toolthe CDN is only as useful as your cache hit ratio
A CDN earns its keep on static, shared, immutable bytes. The trap for an AI dev tool is that the high-value traffic is the least cacheable: a user's prompt, their repo context and a streamed model completion are all personalized and one-shot. Be specific about the split.
- Traffic
- App binaries, updater deltas, fonts, marketing site
- Cacheable?
- Yes, aggressively
- Why / how
- Immutable, content-hashed; long TTLs, cache by URL
- Traffic
- Public docs, OG images, static API schemas
- Cacheable?
- Yes
- Why / how
- Shared across users; revalidate on deploy
- Traffic
- Auth'd API responses (settings, entitlements)
- Cacheable?
- Rarely
- Why / how
- Per-user; only with short private TTLs and a vary on identity
- Traffic
- Prompts, retrieval results, model completions
- Cacheable?
- No
- Why / how
- Personalized, one-shot, often streamed token by token
| Traffic | Cacheable? | Why / how |
|---|---|---|
| App binaries, updater deltas, fonts, marketing site | Yes, aggressively | Immutable, content-hashed; long TTLs, cache by URL |
| Public docs, OG images, static API schemas | Yes | Shared across users; revalidate on deploy |
| Auth'd API responses (settings, entitlements) | Rarely | Per-user; only with short private TTLs and a vary on identity |
| Prompts, retrieval results, model completions | No | Personalized, one-shot, often streamed token by token |
For Cursor, the edge wins on delivery and protection more than on caching the hot path.
WAF, TLS termination and mTLS to originwhere encryption ends and where trust begins
TLS should terminate at the edge for handshake latency, then a fresh, mutually authenticated TLS session should carry the request to origin. Terminating once at the edge and sending plaintext over your backbone is the cheap mistake that fails a security review.
Managed rule sets for OWASP Top 10: SQLi, XSS, path traversal.
Custom rules for your shapes: oversized payloads, malformed auth headers, suspicious user agents.
Run in count mode first, then enforce, so you don't 403 real users on day one.
Terminate TLS 1.3 at the edge POP near the user.
Re-encrypt edge→origin; ideally mTLS so origin only trusts the edge.
Then mTLS again service-to-service inside the mesh.
The governing principle: enforce security and rate limiting as far from origin as you can. Every request you reject or serve at the edge is compute, bandwidth and GPU time you didn't spend. At 1M+ DAU, shedding 5% of abusive traffic at the edge is a real line item, not a rounding error.
Open the design by drawing the layers and stating your defense-in-depth posture: "TLS at the edge, WAF and rate limiting before origin, mTLS to origin, mTLS in the mesh." It shows you think in layers, not in a single firewall and it frames every later decision as "which layer owns this?"
Takeaway. A request crosses DNS → Anycast/POP → CDN → WAF/rate-limit → origin; cache static delivery, shed abuse at the edge and re-encrypt edge→origin so the hot inference path is protected without being slowed.
Self-check
QAn interviewer asks why a CDN won't save you much on Cursor's model-completion traffic. What's the crisp answer?
Rate limiting and abuse defense
After this you can design rate limiting that protects the platform without hurting good users.
Rate limiting is where infra meets unit economics. Every model call has a hard dollar cost, so a runaway script or a stolen API key isn't just a reliability problem - it's a bill.
The interviewer wants to see that you can pick an algorithm for a reason, run it across many edge nodes without a hot key meltdown and degrade kindly when a real user brushes the limit. Start with the algorithms.
- Algorithm
- Fixed window
- Burst behavior
- Allows a 2x spike at the window boundary
- Tradeoff
- Simplest and cheapest; unfair edge effect lets bursts through
- Algorithm
- Sliding window
- Burst behavior
- Smooths the boundary spike
- Tradeoff
- Fairer; needs per-request timestamp state or a weighted approximation
- Algorithm
- Token bucket
- Burst behavior
- Allows controlled bursts up to bucket size, then steady refill
- Tradeoff
- Best UX for bursty clients; two params (rate, burst) to tune
- Algorithm
- Leaky bucket
- Burst behavior
- No bursts; constant drain rate
- Tradeoff
- Protects a fragile backend with fixed throughput; can feel sluggish
| Algorithm | Burst behavior | Tradeoff |
|---|---|---|
| Fixed window | Allows a 2x spike at the window boundary | Simplest and cheapest; unfair edge effect lets bursts through |
| Sliding window | Smooths the boundary spike | Fairer; needs per-request timestamp state or a weighted approximation |
| Token bucket | Allows controlled bursts up to bucket size, then steady refill | Best UX for bursty clients; two params (rate, burst) to tune |
| Leaky bucket | No bursts; constant drain rate | Protects a fragile backend with fixed throughput; can feel sluggish |
Token bucket is the usual default for user-facing APIs: it forgives a quick burst, then enforces a sustained rate.
For Cursor, token bucket fits the human pattern: a developer fires several agent requests in a flurry, then goes quiet to read the diff. You want to allow the flurry and cap the sustained rate, which is exactly the burst + refill shape.
Distributed rate limiting and hot keysthe part that breaks at scale
One counter in memory is easy. The hard version is hundreds of edge nodes that must agree on whether a user is over their limit, in single-digit milliseconds, without a central counter becoming a bottleneck. Two real problems show up.
A single heavy user (or a viral repo's CI) hammers one counter shard.
Mitigate with local token buckets per node plus periodic reconciliation to a shared store.
Accept small over-admission as the price of not centralizing every decision.
A low-latency store (Redis-style) holds the authoritative count.
Use atomic ops so concurrent nodes don't double-count.
Async replication across regions; per-region local enforcement to avoid a cross-region round trip on every request.
Choose the right limiting dimension
- Per API key
- Best for programmatic access and billing; the key is the identity and the cost center.
- Per user / account
- Maps limits to entitlements and plan tier; survives IP changes.
- Per IP
- Last line against anonymous floods; weak alone (NAT, shared offices, CGNAT) so use as a coarse net, not the primary control.
Real systems layer all three: per-key for cost, per-account for fairness, per-IP for the anonymous edge.
Abuse, DDoS and protecting expensive backendsthe inference path is the crown jewel
DDoS volumetrics get absorbed by Anycast and scrubbing at the edge. The subtler threat is abuse that looks legitimate: a credential-stuffed account or a leaked token driving thousands of real, expensive model calls. Defend the wallet, not just the wire.
- Tier limits by cost: a cheap autocomplete endpoint gets a generous bucket; a long-context agent run gets a tight one tied to plan.
- Add an anomaly signal on spend velocity per account, not just request count, so a sudden cost spike trips a challenge.
- Use challenge flows (a managed bot check) for suspicious clients before you outright block, to avoid false-positive lockouts.
- Return honest 429s with a Retry-After hint so well-behaved clients back off instead of retry-storming you.
HTTP/1.1 429 Too Many Requests
Retry-After: 8
RateLimit-Limit: 60
RateLimit-Remaining: 0
RateLimit-Reset: 8
Content-Type: application/json
{
"error": "rate_limited",
"scope": "per_account",
"retry_after_seconds": 8
}Don't rate-limit purely on IP for an auth'd developer tool. Whole companies sit behind one egress IP, so an IP cap throttles a paying customer's entire office while a botnet across 10,000 residential IPs sails under it. Limit on the identity that maps to cost and entitlement and keep IP as a coarse backstop.
"I'd put a token-bucket limiter at the edge keyed on account and API key, enforced locally per node with async reconciliation to a shared store so I don't pay a cross-region round trip per request. The expensive inference endpoints get tighter, plan-aware buckets plus a spend-velocity anomaly check, because for us a limit breach is a cost event, not only a load event."
Takeaway. Default to token bucket for bursty human traffic, enforce locally per edge node with async reconciliation to dodge hot keys, key limits on account/API-key (not IP) and protect inference endpoints by spend velocity, not just request count.
Self-check
Multi-region and multi-cloud geo-deployment
After this you can choose a regional topology that meets latency and resilience goals.
Going multi-region is a tax you pay for two things: lower latency for distant users and survival when a region fails. Pick the topology that buys the resilience you actually need, not the most impressive diagram.
The first fork is active-active versus active-passive. The honest framing is cost and operational complexity against how fast you must recover. Lay it out before you commit.
- Traffic
- Active-active
- All regions serve live traffic
- Active-passive
- One region serves; others stand by
- Failover time
- Active-active
- Near-zero; just stop steering to the dead region
- Active-passive
- Minutes; promote standby, warm caches, repoint DNS
- Cost
- Active-active
- High - full capacity running everywhere
- Active-passive
- Lower - standby can be smaller or scaled down
- Data complexity
- Active-active
- Hard - multi-writer conflicts, replication lag matters
- Active-passive
- Simpler - single writer, async replica promoted on failover
- Best when
- Active-active
- Latency for a global user base is first-order
- Active-passive
- Resilience matters but a few minutes of failover is tolerable
| Active-active | Active-passive | |
|---|---|---|
| Traffic | All regions serve live traffic | One region serves; others stand by |
| Failover time | Near-zero; just stop steering to the dead region | Minutes; promote standby, warm caches, repoint DNS |
| Cost | High - full capacity running everywhere | Lower - standby can be smaller or scaled down |
| Data complexity | Hard - multi-writer conflicts, replication lag matters | Simpler - single writer, async replica promoted on failover |
| Best when | Latency for a global user base is first-order | Resilience matters but a few minutes of failover is tolerable |
Active-active for latency and instant failover; active-passive when a controlled few-minute recovery is acceptable for far less cost and complexity.
Data locality and replicationthe part that decides whether multi-region even works
Stateless services are easy to spread; data is the hard constraint. A cross-region database call adds 60–150 ms of speed-of-light tax every time, so a chatty path that hops the Atlantic per request will be slow no matter how good your edge is. The design goal is to keep each request's data reads inside its own region.
- Read-local, write-where-it-makes-sense: serve reads from a same-region replica; route writes to the owning region or use a database built for multi-region writes if conflicts are rare.
- Pin a user's session to a home region so their hot data stays close and you avoid replication-lag surprises mid-session.
- Keep large/cold artifacts (indexes, blobs) in regional object storage replicated lazily, not in the synchronous request path.
- Name your consistency choice out loud: strong within region, eventual across regions and which user-visible operations can tolerate which.
Failover and traffic steeringtested, not theoretical
Steering lives in two places: DNS / global load balancer health checks decide which region is even an option and the edge picks among the healthy ones. The credibility line is whether failover is tested.
- 1Health checks at the region level probe a real dependency path, not just a port being open - a region that can't reach its database is unhealthy even if its ALB answers.
- 2Global LB / geo-DNS removes an unhealthy region from rotation and steers users to the next-nearest healthy one.
- 3Drain and rebalance so the surviving regions don't fall over from the displaced load - capacity-plan for N-1.
- 4Game-day it. Kill a region on purpose in a drill; an untested failover is a hope, not a control.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Steering lives in two places - health checks decide which regions are even an option, the edge picks among the healthy ones. The gate is the line that separates a real control from a hope.
If you run two active regions at 60% each, losing one dumps its load onto the survivor and you're instantly at 120% - an outage caused by your own failover. Multi-region resilience only counts if the remaining regions can absorb the dead one's traffic. State that you'd capacity-plan and autoscale for N-1.
Multi-cloud: when the operational tax is worth ita second provider is a big commitment
Multi-region on one cloud handles most resilience needs. A second cloud provider doubles your IAM models, networking primitives and on-call surface and you lose the deep integrations that make one cloud productive. Reach for it only with a concrete driver.
A specific resource is only available or affordable elsewhere - e.g. GPU capacity for inference at a price or scale your primary cloud can't meet.
A customer or regulatory requirement mandates provider diversity.
Provider-level outage risk is genuinely existential to the business.
You just want "resilience" - multi-region on one cloud already gives you that for far less.
You'd run a lowest-common-denominator stack and forfeit managed services to stay portable.
The team is small and flat - every added platform is permanent on-call and toil.
When asked "would you go multi-cloud?", resist the heroic yes. The senior answer is: "Multi-region on AWS first - it covers latency and resilience. I'd add a second provider only for a concrete driver like GPU supply for inference and I'd scope it to that workload rather than mirroring the whole platform." Decisive, cost-aware and scoped beats ambitious and vague.
Takeaway. Pick active-active for latency and instant failover, active-passive when a few-minute recovery is fine; keep reads region-local, capacity-plan for N-1, game-day your failover and treat a second cloud as a scoped decision for a concrete driver, not a default.
Self-check
QA candidate proposes two active regions each running at 65% utilization for "high availability." What flaw do you point out?
Latency budgeting end to end
After this you can account for every millisecond from user to response and back.
Cursor is latency-obsessed by design and it shows in the interview. "Make it fast" is a junior answer. "Here's my millisecond budget and where I'd spend or save" is the one that lands.
Break the round trip into named components, give each a budget and treat the sum as a contract. When you blow the budget, you'll know exactly which segment to attack instead of guessing.
- DNS
- ~0–20 ms - usually cached on the client after first resolve
- TLS handshake
- ~0–30 ms - amortized to near-zero with connection reuse / 0-RTT
- Edge processing
- ~5–15 ms - WAF, rate-limit check, routing
- Network (user↔region)
- ~20–80 ms - dominated by physical distance; the case for regional proximity
- Queueing / scheduling
- ~5–40 ms - grows fast under saturation; the silent p99 killer
- Backend processing
- the remainder - your actual work; for inference this dwarfs everything
These are illustrative, not Cursor's real numbers - the point is to reason in named segments with explicit budgets.
Inference is its own world: a streamed completion's time-to-first-token matters more than total time, because the user sees motion immediately. Budget the first token tightly and let the stream fill in.
p50 versus p99: design for the tailaverages lie at scale
At 1M+ DAU, the p99 is not a rare edge - it's tens of thousands of requests an hour and it's disproportionately your most engaged users hitting it repeatedly. A great median with an ugly tail still feels broken.
- Tail cause
- Queueing under load
- Symptom
- p99 explodes while p50 looks fine
- Mitigation
- Autoscale on the right signal; shed/queue with limits; add headroom
- Tail cause
- Cold starts
- Symptom
- First request to a scaled-up pod is slow
- Mitigation
- Pre-warm, keep a warm pool, scale earlier on leading signals
- Tail cause
- Cross-region hop
- Symptom
- A subset of users always slow
- Mitigation
- Regional pinning so data and compute sit near the user
- Tail cause
- GC / noisy neighbor
- Symptom
- Random slow requests, no pattern
- Mitigation
- Right-size, isolate, hedge with a second request on timeout
| Tail cause | Symptom | Mitigation |
|---|---|---|
| Queueing under load | p99 explodes while p50 looks fine | Autoscale on the right signal; shed/queue with limits; add headroom |
| Cold starts | First request to a scaled-up pod is slow | Pre-warm, keep a warm pool, scale earlier on leading signals |
| Cross-region hop | A subset of users always slow | Regional pinning so data and compute sit near the user |
| GC / noisy neighbor | Random slow requests, no pattern | Right-size, isolate, hedge with a second request on timeout |
Tail latency is usually a systems problem (queueing, placement), not slow code.
Techniques to buy milliseconds backwhere the savings actually are
- Connection reuse and HTTP/2 or HTTP/3: kill repeated handshakes - often the cheapest big win for chatty clients.
- Regional pinning: put the user, their data and their compute in one region so the network segment stays small.
- Cache the cacheable at the edge so those requests never pay backend or network cost at all.
- Shed work to the edge: do auth, rate-limit and routing decisions near the user and short-circuit early.
- Hedged requests for the tail: on a slow outlier, fire a second request and take the first to return - spend a little compute to cut p99.
The discipline is to set the budget first, instrument each segment and then make spend/save decisions against data. "We added 30 ms of edge auth to remove a 90 ms cross-region database call, net 60 ms faster" is the kind of explicit tradeoff that reads as a Cursor-grade infra engineer.
Don't optimize the median and call it done. If you only quote p50 in the room, expect the follow-up "what about p99?" Lead with the tail: name your queueing and placement risks before you're asked and you've shown the instinct the team actually screens for.
Takeaway. Split the round trip into DNS, TLS, edge, network, queueing and backend, give each a budget and design for p99 (queueing and placement) not p50 - then justify every change as an explicit ms spend/save against data.
Self-check
Reliability: SLOs, error budgets, observability
After this you can frame edge and regional decisions in SLO terms.
Everything in this module - the edge, the limits, the regions, the budgets - should hang off one number: the SLO. It turns "is this reliable enough?" from a vibe into a decision you can defend.
An SLI is what you measure, an SLO is the target you commit to and the error budget is the allowed shortfall. The budget is the lever: spend it on shipping when you're healthy, freeze and stabilize when you've burned it.
- SLI
- A measured ratio of good events to total - e.g. fraction of requests under 300 ms or fraction returning non-5xx.
- SLO
- The target on that SLI - e.g. 99.9% of requests succeed over 28 days.
- Error budget
- 100% minus the SLO - 0.1% here, about 40 min/month of allowed failure. It's a budget you're meant to spend, not hoard.
The cultural payoff fits Cursor's bias to ship: a healthy error budget is permission to move fast and take risks. A burned budget is the signal to slow down and harden. The number, not opinion, arbitrates the ship-versus-stabilize fight.
Golden signals and alerting on symptomsalert on what users feel
- Golden signal
- Latency
- What it catches
- Things getting slow
- Example edge/region metric
- p99 of edge→origin and time-to-first-token
- Golden signal
- Traffic
- What it catches
- Demand shifts and floods
- Example edge/region metric
- Requests/sec per region and per route
- Golden signal
- Errors
- What it catches
- Things breaking
- Example edge/region metric
- 5xx rate, WAF block rate, 429 rate
- Golden signal
- Saturation
- What it catches
- Running out of headroom
- Example edge/region metric
- CPU/GPU utilization, queue depth, connection pool use
| Golden signal | What it catches | Example edge/region metric |
|---|---|---|
| Latency | Things getting slow | p99 of edge→origin and time-to-first-token |
| Traffic | Demand shifts and floods | Requests/sec per region and per route |
| Errors | Things breaking | 5xx rate, WAF block rate, 429 rate |
| Saturation | Running out of headroom | CPU/GPU utilization, queue depth, connection pool use |
Page on symptoms users feel (latency, errors). Causes (a full disk, high CPU) are dashboards to consult, not pages at 3am.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
The four golden signals ranked by how hard you page on them - user-facing symptoms page, capacity is a leading indicator, raw demand mostly informs.
The discipline behind "alert on symptoms, not causes" is that one user-facing symptom can have many causes and you'll burn out an on-call rotation paging on every cause that might one day matter. Page when the SLO is at risk; investigate the cause once you're already awake.
Graceful degradation and circuit breakersa partial outage shouldn't be a total one
When a dependency or a region fails, the goal is to lose a feature, not the product. Circuit breakers stop you from hammering a sick dependency and dragging healthy paths down with it.
Trip after a failure threshold so calls fail fast instead of piling up.
Half-open probes test recovery before fully reopening.
Pair with timeouts and bounded retries (with jitter) so retries don't become a self-inflicted DDoS.
If retrieval is down, answer with reduced context rather than erroring out.
If a region is down, steer to the next-nearest and accept slightly higher latency.
Shed non-essential work first so the core path keeps its budget.
Observability across regionsyou can't debug what you can't trace
A request that crosses edge → mesh → service across regions is invisible without distributed tracing. Propagate a trace context from the edge down and correlate it with metrics and logs so one slow request tells you which hop ate the time.
- 1Inject a trace/request ID at the edge and carry it through every hop and every log line.
- 2Trace the path edge → ingress → mesh sidecar → service → datastore, with spans you can read as a waterfall.
- 3Tag spans by region and node so a region-localized problem stands out instead of averaging away.
- 4Correlate the three pillars - jump from a latency metric to the exemplar trace to the logs for that exact request.
Tie your design back to an SLO unprompted. "This edge change protects the latency SLO; this failover plan protects the availability SLO; here's how I'd alert when either budget burns fast." Framing decisions as SLO-driven shows you optimize for what users feel and for defensible tradeoffs, which is exactly the judgment Cursor screens infra hires for.
Don't alert when errors cross a fixed line. Alert on error-budget burn rate: a fast burn (you'll exhaust the month's budget in an hour) pages immediately; a slow burn opens a ticket. It catches both the sudden outage and the steady erosion without drowning on-call in noise.
Takeaway. Commit to SLIs/SLOs and let the error budget arbitrate ship-vs-stabilize; page on golden-signal symptoms (latency, errors) via burn rate, degrade gracefully with circuit breakers and trace every request edge→mesh→service so cross-region failures are debuggable.
Self-check
QWhy does mature on-call alert on the error-budget burn rate rather than on a fixed error-count threshold?