GCP Lesson 80 of 98

Education Platform Scaling for a National Exam Day on GCP

In a nutshell

Imagine a national concert where 60,000 fans all arrive in the same five minutes because the doors open at exactly 7:00 pm. If you hire staff and open gates only after the queue forms, you get a dangerous crush. But you know the doors open at 7:00, so you do the opposite: every gate open, every steward already in position, and tickets people scan themselves — all before the first fan walks up. That is this entire lesson. A national exam is that concert, except 1.4 million students press “Start” inside the same ninety seconds, and the “crush” is a website showing a spinner while someone’s university place slips away.

The hard part is not how much traffic — plenty of sites serve more over a day — it is the shape of it: a near-vertical wall from almost nothing to peak, at a second you can predict on the calendar. That predictability is the gift the whole design is built around. Because you know the exact moment, you pre-warm (bring capacity up before the spike instead of reacting to it), cache the unchanging parts at the edge so they never touch your servers, stand up a layered shield against attackers who love a high-stakes moment, and make sure the one thing that must never fail — saving a candidate’s answer — is written somewhere durable the instant it happens.

Everything below is how you compose Google Cloud’s building blocks — GKE for compute, Cloud Spanner for never-lose-a-write storage, Memorystore for a fast in-memory buffer, a global load balancer with Cloud Armor and a CDN at the edge, and Pub/Sub for coordination — to survive that ninety seconds. The same playbook scales down to a flash sale, a concert ticket on-sale, results day, or a tax-filing deadline: any spike you can see coming, you can pre-stage for.

Level: Advanced · Time: ~38 min

Prerequisites. You’ll get the most from this if you’re comfortable with the basics of cloud compute (virtual machines and containers), what a load balancer and a database do, and the idea of horizontal scaling (running more copies of a service). It helps to know roughly what Kubernetes and GKE are and what “autoscaling” means — but every term is defined in the Glossary at the end, so a determined beginner can follow the whole thing.

After this lesson you’ll be able to:


A national testing authority that runs a country’s university-entrance exam gives its platform team a deadline with no slack in it: on a single Saturday in May, 1.4 million candidates will press “Start” within the same ninety-second window, because the exam is timed identically nationwide and the proctors release it on a synchronized clock. The board chair has lived through the alternative — two years ago the legacy provider’s load balancer melted under the thundering herd, candidates stared at spinners for eleven minutes, the press called it a “rigged exam,” and the resulting court case forced a full re-sit at a cost that dwarfs a decade of cloud bills. The new mandate is unambiguous: the platform must absorb a cold-start spike from near-zero to peak in under two minutes, stay up through a hostile internet, and never lose a single submitted answer — because a lost answer is not a glitch, it is a candidate’s university place. This is the reference architecture for building that platform on Google Cloud, where the engineering problem is not steady-state throughput but the brutal, unforgiving shape of the load curve.

The pressures here are unlike a normal SaaS app, and naming them sets up every decision that follows. The spike is synchronous and predictable to the minute — you know the exact Saturday and the exact second, which is a gift, because it means you can pre-warm instead of react. The load is bursty, not sustained — peak concurrency lasts three hours and is near-zero the rest of the quarter, so paying for peak capacity year-round is indefensible. Correctness is sacred — answer autosave must be durable and globally consistent, because a candidate who answers in one region and reconnects through another must see their work. And the event is a magnet for attack — a DDoS during the entrance exam, or a single cheater scripting the API, is worth real money to someone, so the platform is hostile-internet-facing by definition. GCP gives you the primitives — GKE, Cloud Spanner, Memorystore, Cloud Armor — but the architecture is in how you compose them for this curve.

Why the obvious approaches fail

Three shortcuts will be proposed in the first planning meeting, and each fails in a way worth naming before someone builds it.

“Just turn on the Horizontal Pod Autoscaler and let it react.” Reactive autoscaling watches CPU, notices the spike, and then asks for pods — but GKE has to schedule pods onto nodes, and if the nodes do not exist, the Cluster Autoscaler must call Compute Engine to provision VMs, pull container images, and pass readiness probes. That control loop takes minutes you do not have when the entire country starts in ninety seconds. By the time capacity arrives, the spike is over and the candidates have already seen the spinner. Reactive scaling is a strategy for gradual load; it is a guaranteed outage for a synchronized cold start.

“Run it on a single regional SQL database.” A national exam means national users, and a single Cloud SQL primary becomes the write bottleneck for 1.4 million autosaving sessions while also being a single point of failure and a single region of latency. Sharding it by hand reintroduces every distributed-systems problem you were trying to avoid, and a failover mid-exam loses in-flight transactions — the one thing you swore would never happen.

“Cache everything and hope.” Putting a CDN in front and calling it done handles the static exam shell, but the exam interaction — fetching the next question, saving an answer, syncing a timer — is dynamic, per-candidate, and write-heavy. You cannot cache a write. The dynamic path is the whole problem, and a CDN does not touch it.

The architecture below threads the needle: pre-provision against a known schedule instead of reacting, use a horizontally-scalable consistent database built for exactly this write pattern, absorb reads at the edge and in-memory, and put the dynamic write path behind autoscaling compute that was warmed before the gun fired.

Architecture overview

Education Platform Scaling for a National Exam Day on GCP — architecture

Trace the diagram left to right as a single request’s journey at T-0: a candidate’s browser hits Akamai at the edge, only genuine dynamic calls pass the global load balancer and Cloud Armor, the session authenticates, lands on the pre-scaled GKE exam-session service, reads and writes hot state in Memorystore, and commits every answer durably to Cloud Spanner — while the proctor path stays a separate, higher-privilege lane. The platform separates three traffic classes that scale on entirely different curves, and keeping them distinct is the first discipline of operating it well: the static delivery path (the exam shell, JS bundles, instructions — heavy on read, trivially cacheable), the dynamic exam path (fetch question, autosave answer, sync timer — the write-heavy core that everything else exists to protect), and the proctoring/admin path (invigilators monitoring sessions, releasing the exam, handling incidents — low volume, high privilege).

The defining property of the whole topology is the one the board cares about: capacity is provisioned ahead of a known clock, not discovered after the spike. This is a scheduled event, so the architecture treats the exam start like a rocket launch — everything is warm, primed, and load-tested before T-0.

Dynamic exam path, following the request flow at T-0:

  1. A candidate’s browser, already showing the pre-loaded exam shell, hits Akamai at the edge. Akamai serves all static assets from cache (so 1.4 million shell loads never touch GCP), terminates TLS, and runs as the first DDoS and bot-mitigation layer with rate controls tuned for exam-flood patterns. Only genuine dynamic API calls are forwarded to the origin.
  2. Forwarded traffic reaches Google Cloud Load Balancing (global external Application Load Balancer) with Cloud Armor attached as the second, GCP-native defense layer — WAF rules, per-IP and per-token rate limiting, and Adaptive Protection ML-driven L7 DDoS detection. Cloud Armor is also where a scripted cheater hammering the answer-submit endpoint gets throttled before it reaches the application.
  3. The candidate’s session is authenticated by a token minted at login. Candidates authenticate through the exam platform’s own identity service backed by Identity Platform, while proctors and administrators sign in through Okta as the workforce IdP — Okta enforces MFA and conditional access for invigilators, and federates to GCP so an invigilator’s privileged session is a first-class, auditable identity separate from any candidate.
  4. The request lands on the exam-session service running on GKE — a regional, multi-zone cluster that was pre-scaled to peak before the exam window opened (more on the predictive mechanism below). The service reads and writes the candidate’s live exam state.
  5. Hot, per-session state — the current question pointer, the server-authoritative countdown timer, recent autosaves — lives in Memorystore for Redis, which absorbs the brutal read/write rate of timers ticking and answers saving without hammering the database on every keystroke.
  6. The durable answer of record is written to Cloud Spanner. Every autosave is an upsert into Spanner, which gives horizontal write scalability and external (globally strong) consistency — so a candidate who drops Wi-Fi and reconnects through a different region sees exactly the answers they saved, with no lost write and no stale read. This is the property that lets the authority promise “we will never lose your answer.”
  7. Secrets the services cannot derive from workload identity — third-party proctoring-vendor API keys, the Okta introspection secret, exam-content decryption keys — come from HashiCorp Vault via a sidecar with GCP-backed auth, so nothing sensitive sits in a Kubernetes Secret. Critically, the encrypted exam paper itself is decrypted only at the synchronized release time, with Vault holding the key until the clock says go.

Proctoring/admin path, lower volume and higher privilege: invigilators use a console (Okta-gated) to monitor live sessions, flag anomalies, grant time accommodations, and — the highest-stakes action — trigger the synchronized exam release. That release is a control-plane event that flips a flag in Spanner and pushes via the real-time channel to every candidate at once.

Component breakdown

Component Service / tool Role on exam day Key configuration choices
Edge / static Akamai Serve cached exam shell to 1.4M browsers; first DDoS/bot layer Cache static bundles; flood rules on dynamic API; origin shield to GCLB
L7 load balancing + WAF Cloud Armor + Global ALB GCP-native WAF, rate limiting, Adaptive Protection DDoS Per-token rate limits; Adaptive Protection on; preconfigured OWASP rules
Candidate identity Identity Platform Candidate auth, session tokens, sign-in throttling Short-lived tokens; per-IP sign-in limits; passwordless option
Workforce SSO Okta Proctor/admin SSO, MFA, conditional access, federation to GCP OIDC federation; step-up MFA for “release exam”; group claims to authz
Exam-session compute GKE (regional, multi-zone) The dynamic exam logic: fetch question, autosave, timer sync Pre-scaled to peak; PDB; multi-zone spread; node pools warmed
Hot session state Memorystore for Redis Live timer, question pointer, recent autosaves; read shock absorber Standard tier (HA); read replicas; per-session keyspace
Durable answers Cloud Spanner Globally consistent answer-of-record; horizontal write scale Multi-region or regional+read replicas; interleaved session tables
Real-time channel Pub/Sub + WebSocket gateway Synchronized release, time warnings, proctor pushes Fan-out to candidate connections; backpressure handling
Secrets & exam keys HashiCorp Vault Vendor keys, Okta secret, exam-paper decryption key GCP auth method; dynamic leases; key released at start time only
Predictive scaling Custom controller + Cloud Monitoring Pre-warm nodes/pods against the known schedule Scheduled scale-up; custom metric HPA; surge node pool
CSPM / posture Wiz + Wiz Code Cloud posture, exposure, IaC scanning before exam day Agentless scan of GKE/Spanner; Wiz Code gates Terraform PRs
Runtime security CrowdStrike Falcon Runtime threat detection on GKE nodes Sensor on node pools; detections to the SOC
Observability Datadog Real-time dashboards, the war-room view, SLO burn alerts Agent on GKE; RUM on candidate client; live concurrency metric
ITSM / incident ServiceNow Exam-day incident bridge, change gate, accommodation tickets Major-incident workflow; change freeze gate; auto-ticket on SLO breach
CI / IaC GitHub Actions + Argo CD + Terraform Build/test/load-test pipeline; GitOps deploy; infra as code OIDC to GCP (no stored creds); Argo CD syncs cluster; load-test gate

A few of these choices carry the architecture and deserve the why, because they are the ones teams get wrong on a spike workload.

Why predictive (scheduled) autoscaling, not reactive. Because the exact start time is known, the platform does not wait for CPU to climb — a scheduled scale-up drives the Cluster Autoscaler and HPA to peak capacity before the exam window, holds it warm through the event, and scales back down after. The Horizontal Pod Autoscaler is still configured, but on a custom metric — active exam sessions — not CPU, because CPU lags the real signal and active-session count rises with the herd. A dedicated surge node pool is provisioned and warmed (images pre-pulled, a low-priority “balloon” Deployment holding the nodes) so that when real pods land they schedule in seconds, not minutes. The mental model: you are not autoscaling into the spike, you are pre-staging for it and using autoscaling only to trim.

# HPA on a business metric (active exam sessions), with a high floor
# pre-staged for the known start time — not reacting to CPU.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: exam-session-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: exam-session
  minReplicas: 600        # pre-warmed floor, raised by the scheduled job before T-0
  maxReplicas: 1200
  metrics:
    - type: Pods
      pods:
        metric:
          name: active_exam_sessions_per_pod
        target:
          type: AverageValue
          averageValue: "1200"
  behavior:
    scaleUp:
      policies:
        - type: Percent
          value: 100
          periodSeconds: 30   # allow fast doubling if the floor is under-set

Why Cloud Spanner instead of Cloud SQL. The autosave workload is a high-volume, geographically distributed write stream that must be strongly consistent and must never lose a committed transaction. Spanner is purpose-built for exactly this: it scales writes horizontally by splitting across nodes, gives external consistency (the strongest guarantee, so reads always reflect all prior writes globally), and survives a zone or region failure without losing committed data. A single Cloud SQL primary cannot scale the writes, and its failover window is precisely when you would lose answers. The cost is real — Spanner is more expensive per hour and demands schema discipline — but for “never lose a candidate’s answer,” it is the only primitive that delivers the guarantee.

Why Memorystore sits in front of Spanner. A server-authoritative countdown timer for 1.4 million candidates, ticking and being read constantly, plus autosaves landing every few seconds, would generate a punishing read/write rate straight onto the database. Memorystore for Redis absorbs that: the live timer and current-question pointer live in Redis, autosaves write to Redis immediately for instant client acknowledgment and are flushed durably to Spanner, and reads are served from memory. Redis is the shock absorber that keeps Spanner doing durable writes rather than serving hot per-keystroke reads. The tradeoff is a consistency seam — Redis is fast but volatile — so Spanner remains the answer of record and the flush path is designed to never drop a write.

Implementation guidance

Provision with Terraform, gate it with Wiz Code, deploy with Argo CD. Infrastructure is code, and on a workload where a misconfiguration is a national incident, the pipeline is part of the safety case.

  1. A regional, multi-zone GKE cluster (private nodes, Workload Identity on) with two node pools: a baseline pool and a surge pool sized to peak, kept warm by a balloon Deployment.
  2. Cloud Spanner with the exam schema — interleave the per-answer rows under the session row so a candidate’s session and all their answers co-locate, keeping autosave writes to a single split where possible.
  3. Memorystore for Redis Standard tier (HA with automatic failover) with read replicas sized to the timer/read load.
  4. Cloud Armor security policy attached to the global ALB, Adaptive Protection enabled, with explicit per-token rate-limit rules on the autosave and submit endpoints.
  5. Akamai in front, caching static assets and pointing at the GCLB private origin as shielded backend.

The pipeline runs in GitHub Actions, authenticating to GCP via OIDC Workload Identity Federation so there is no long-lived service-account key to leak — a lesson the platform team intends never to relearn. Wiz Code scans every Terraform and Kubernetes manifest PR for misconfigurations (a public Spanner endpoint, an over-broad firewall, a missing Pod Security control) and blocks the merge if it finds one. Argo CD then syncs the desired cluster state from Git, so the production cluster is always a known, reviewed commit — no out-of-band kubectl apply on exam morning.

The load test is the deliverable, not an afterthought. You cannot claim to survive 1.4 million synchronized starts; you have to demonstrate it. A required pipeline gate runs a distributed load test that reproduces the curve — near-zero to peak in ninety seconds — against a production-clone environment, asserting p99 autosave latency and zero dropped writes. A scale event that has not been load-tested at the real shape is an unvalidated assumption, and on this platform unvalidated assumptions become court cases.

Synchronized release without a thundering-herd self-DDoS. The exam content is encrypted at rest; Vault holds the decryption key and releases it only at the start time, so even an insider cannot read the paper early. At T-0 the release flips a flag in Spanner and fans out over Pub/Sub to a WebSocket gateway that pushes to already-connected candidates — candidates are connected and idle before the start, so the gun does not trigger 1.4 million simultaneous new connections, only a lightweight push over existing ones. Pre-connecting the herd is what converts a connection storm into a trivial fan-out.

Enterprise considerations

Security & Zero Trust. The platform is hostile-internet-facing on its biggest day, so defense is layered and identity is strict. Akamai and Cloud Armor form two independent DDoS/WAF tiers — Akamai absorbs volumetric and static-flood attacks at the edge, Cloud Armor’s Adaptive Protection catches application-layer (L7) attacks and rate-limits the autosave/submit endpoints so a scripted cheater is throttled before reaching the app. Privileged actions are gated hard: invigilators authenticate through Okta with MFA, and the highest-stakes action — releasing the exam — requires step-up authentication, so no single stolen session can leak the paper. HashiCorp Vault holds the exam-paper key and releases it only at start time, making “early access to questions” a non-event. Wiz runs continuous CSPM across GKE and Spanner, alerting on any drift to public exposure or an over-permissive IAM binding, while Wiz Code shifts that same checking left into the IaC pipeline. CrowdStrike Falcon sensors on the GKE node pools provide runtime threat detection feeding the testing authority’s SOC, and any security event auto-raises a ServiceNow incident so there is a ticket and a bridge, not just a log line. Least-privilege IAM scopes each service to exactly the resources it needs, and candidate and proctor identities are separate trust domains by construction.

Cost optimization. The economics of a once-a-quarter spike are the whole game: peak capacity for three hours, near-zero for months. Engineer for the curve, not the peak.

Lever Mechanism Typical effect on exam-day economics
Scheduled scale-up/down Pre-warm to peak before T-0, scale to a small floor after Pay for peak only during the ~4-hour window, not year-round
Surge node pool on Spot Run the warmed surge pool on Spot/preemptible where the workload tolerates it Large discount on the burst capacity that is idle most of the time
Spanner sizing per event Scale Spanner nodes up for the event, down after; use processing units granularly Avoid paying for peak write throughput when no exam is running
Memorystore offload Serve timers/reads from Redis so Spanner is sized for durable writes only Smaller, cheaper Spanner footprint for the same correctness
Edge offload Akamai serves 100% of static shell loads 1.4M shell fetches never bill as GCP egress or compute
Commitment vs. on-demand Committed-use discounts for the steady baseline, on-demand for the surge Lowest blended rate across a spiky year

The discipline is to treat the surge as ephemeral: provision it the day before, tear it down the day after, and never let “we might need it” leave peak capacity running into the next quarter. Pipe the per-window cost to Datadog so finance sees exactly what each exam day costs.

Scalability and the spike, concretely. Each tier scales on its own axis. GKE scales pods on the active-sessions custom metric and nodes via the Cluster Autoscaler against the warmed surge pool, so capacity is present before load. Spanner scales by adding nodes/processing units, raised for the event window. Memorystore scales reads via read replicas. The real ceiling to plan against is not CPU but the regional quotas — Compute Engine instance quota, in-use IP addresses, Spanner node limits — which is why the load test runs against the real project with the real quotas, and why a quota-increase request goes in weeks early. A spike architecture that hits an unraised quota at T-0 fails exactly as badly as one with no autoscaling at all.

Failure modes, and what each one looks like on the day. Name them before they page the war room.

Reliability & DR (RTO/RPO). The numbers are decided per tier and the bar is exceptional because a failed exam is not retryable in the moment. Cloud Spanner multi-region gives synchronous replication and survives a full region loss with zero RPO for committed answers — the non-negotiable guarantee. GKE is regional and multi-zone, so a zone failure is transparent; a region failure fails over to a warm standby cluster the load balancer can route to. Memorystore Standard tier provides automatic cross-zone failover. A pragmatic target for the exam-day service: RTO under 5 minutes, RPO zero for submitted answers, with the explicit design principle that a submitted answer is durable the instant Spanner commits it, region failure included. The exam itself can tolerate a brief connectivity blip for a candidate (the client buffers and re-syncs) far more easily than it can tolerate a single lost commit — so the architecture spends its reliability budget on durability first.

Observability and the war room. Exam day is run from a live war room, and Datadog is the single pane: real-time active-concurrency (the metric that proves the herd arrived and capacity held), p99 autosave latency, Spanner CPU and commit latency, Redis hit rate, Cloud Armor blocked-request rate, and SLO burn-rate alerts that page before candidates feel pain. Real User Monitoring on the candidate client surfaces what actual browsers experience, not just server health. A blocked-request spike or an SLO breach auto-opens a ServiceNow major incident with the bridge details, so the org is in incident response within seconds. The principle: on a scheduled high-stakes event, you watch leading indicators (concurrency rising, latency creeping) and act before the lagging ones (errors, abandons) ever move.

Governance. A strict change freeze goes into effect days before the exam — enforced as a ServiceNow change gate that blocks any non-emergency deploy, with Argo CD ensuring production matches a reviewed Git commit and nothing else. Exam content keys, IAM bindings, and Cloud Armor policies are all version-controlled and reviewed. Every privileged proctor action (release, accommodation grant, session intervention) is logged immutably for audit, because the integrity of the exam — and its defensibility in the inevitable challenge — depends on a complete record of who did what when.

Explicit tradeoffs

Accept these or do not build it. This architecture optimizes for a brutal, scheduled spike, and that focus has costs. Cloud Spanner is more expensive and more demanding than a single SQL database — you pay for horizontal write scale and global consistency in dollars and in schema discipline (get the key design wrong and you hot-spot), and for a small, steady workload it is overkill. Predictive scaling trades simplicity for safety: you must know and trust the schedule and pre-provision against it, which means a balloon Deployment and a warmed surge pool burning some money before the event and the operational ritual of scaling up and tearing down each cycle. The layered Akamai + Cloud Armor defense is two products to license, tune, and test rather than one. The Redis-in-front-of-Spanner design adds a consistency seam you must engineer carefully so a fast acknowledgment never becomes a lost answer. And the whole thing demands a real load test at the real shape, which is itself a significant engineering investment — but skipping it is how the legacy provider ended up in court.

The alternatives, and when they win. If your exams are asynchronous — candidates start whenever they like across a window — the synchronized-spike problem evaporates and you can lean on ordinary reactive autoscaling and a regional database; this entire architecture is overkill. If you are running a small institutional exam (a few thousand students, one university), a single Cloud SQL instance and a modest GKE deployment are simpler, cheaper, and entirely sufficient — graduate to this design only when scale and simultaneity demand it. If the platform is a learning management system rather than a high-stakes exam — think Moodle delivering courseware and quizzes where a momentary blip is an annoyance, not a lawsuit — the correctness and DR bar drops dramatically and a managed Moodle on autoscaled GKE with Cloud SQL is the pragmatic fit. And if you genuinely cannot predict the spike timing, you fall back to reactive scaling with a generous floor and accept the cold-start risk this design is built to eliminate. The architecture here is the destination for a national, synchronized, high-stakes exam; the right starting point depends on which of those three words actually apply to you.

Going deeper

The architecture above is the what; this section is the how it actually behaves under the hood — the mechanisms you configure, the GCP-native options the reference design abstracts over, and the failure physics an experienced engineer needs to reason about before the day.

The three ways to pre-warm, and when to use each

“Pre-warming” is one idea with three concrete implementations on GCP, one per compute model:

gcloud run deploy exam-api \
  --image=REGION-docker.pkg.dev/PROJECT_ID/exam/api:TAG \
  --min-instances=50 \
  --max-instances=1000 \
  --region=REGION
gcloud compute instance-groups managed update-autoscaling exam-mig \
  --set-schedule=exam-saturday \
  --schedule-cron="0 8 * * 6" \
  --schedule-duration-sec=16200 \
  --schedule-time-zone="Asia/Kolkata" \
  --schedule-min-required-replicas=1000

The mental model is identical across all three: raise a guaranteed floor on a schedule, and let reactive autoscaling only trim above it.

Scheduled autoscaling on GKE with KEDA

Vanilla Kubernetes has no time-aware HPA — the HPA reacts to metrics, not the clock. The idiomatic way to add a schedule is KEDA’s cron scaler, which raises minReplicaCount between a start and end cron expression and, crucially, takes the maximum across all its triggers — so you can combine a scheduled floor with a metric-driven trim in one object:

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: exam-session-scaler
spec:
  scaleTargetRef:
    name: exam-session
  minReplicaCount: 50            # off-peak floor
  maxReplicaCount: 1200
  cooldownPeriod: 600
  triggers:
    - type: cron                 # the scheduled pre-warm floor for exam morning
      metadata:
        timezone: Asia/Kolkata
        start: "0 8 * * 6"       # 08:00 Saturday — raise the floor before T-0
        end: "30 12 * * 6"       # 12:30 Saturday — exam over, release the floor
        desiredReplicas: "1000"
    - type: prometheus           # trim above the floor on the real signal (representative)
      metadata:
        serverAddress: http://prometheus.monitoring.svc:9090
        query: sum(active_exam_sessions)
        threshold: "1200"

Because arrival is synchronized, the pre-warm floor must sit close to peak, not a fraction of it — set desiredReplicas near your computed peak pod count so capacity is present when the gun fires, and let the metric trigger add the last few percent if concurrency overshoots. This is the concrete implementation of the reference design’s “minReplicas: 600, raised by the scheduled job before T-0.”

Cloud CDN vs. a third-party edge

The reference design uses Akamai at the edge, but the GCP-native equivalent is Cloud CDN, enabled directly on a backend of the global external Application Load Balancer. For the static exam shell it is often all you need: flip --enable-cdn on the backend service, set explicit Cache-Control on the immutable bundles, and enable cache-fill and negative caching.

Cloud CDN (GCP-native) Third-party edge (Akamai/Cloudflare)
Setup One flag on the LB backend; no extra vendor Separate contract, config, and tuning
Static offload Excellent — caches shell/JS/CSS at Google’s edge Excellent, with a larger independent PoP footprint
DDoS posture Cloud Armor + Adaptive Protection on the same LB A second independent volumetric-absorption tier
When it wins Single-vendor simplicity; GCP-centric team You want an independent scrubbing layer in front of GCP
gcloud compute backend-services update exam-static-backend \
  --enable-cdn \
  --cache-mode=CACHE_ALL_STATIC \
  --global

The design keeps both because two independent DDoS tiers is a deliberate resilience choice on a hostile-internet day — but a smaller exam should start with Cloud CDN alone and add a third party only when the threat model demands independent absorption. Either way the principle stands: the static class must never reach origin.

Queue-based load leveling for everything that isn’t the hot path

The autosave hot path is write-through to Spanner for durability — you do not put a queue in front of the answer of record. But a surprising amount of exam-day work is not latency-critical: grading a submitted paper, generating a submission receipt, emitting analytics, emailing “your exam is submitted,” reconciling proctor flags. Pushing that onto the synchronous path makes every request fatter exactly when you can least afford it. Queue-based load leveling decouples it: the request does the minimum durable write, drops a message, and returns; workers drain the queue at their sustainable rate while the backlog absorbs the spike.

gcloud tasks queues create exam-postsubmit \
  --max-dispatches-per-second=500 \
  --max-concurrent-dispatches=1000 \
  --max-attempts=100

The rule of thumb: synchronous only for what the candidate must see succeed right now (the durable save); everything else goes on a queue and is leveled.

The database connection storm — pooling and read replicas

Even with compute scaled perfectly, a classic exam-day outage is the connection storm: 1,000+ pods each opening a handful of database connections multiplies into tens of thousands, and a Postgres/Cloud SQL instance has a hard max_connections ceiling far below that. The fix is a connection poolerPgBouncer in transaction pooling mode multiplexes thousands of client connections onto a small pool of real backend connections — deployed as a sidecar or a small Deployment in front of the database. On the read side, read replicas offload SELECT traffic from the primary so the primary is reserved for writes. This is the scaling path for the Cloud SQL alternative (the smaller-exam design): see Cloud SQL HA, read replicas & private connectivity for the mechanics.

Spanner sidesteps the raw max_connections wall — clients use a session pool in the client library against a horizontally-scaled backend rather than one primary — which is a large part of why it is chosen here. But the discipline transfers: size the session pool deliberately, watch for pool exhaustion under the herd, and never let per-pod connection counts multiply unbounded.

Graceful degradation — protect the core by shedding the periphery

When demand still exceeds capacity despite every precaution, the platform must degrade, not collapse. Degradation is a deliberate, ordered ladder — decided in advance, not at 09:01 in the war room:

  1. Sample down telemetry — drop RUM/analytics sampling first; it costs the candidate nothing.
  2. Relax autosave frequency — widen the autosave interval under load (the client still buffers locally, so nothing is lost, it just syncs less often).
  3. Serve question content from cache — render questions from the CDN/Memorystore copy rather than a fresh origin fetch.
  4. Disable non-essential UI — rich media, live leaderboards, “students online” counters.
  5. Load-shed non-critical endpoints — return 429 with Retry-After on anything that isn’t the exam, and let Cloud Armor’s throttle rules enforce it at the edge.

The one rung you never shed is the answer-save/submit path. The design principle is that the client is local-first: it writes answers to local storage immediately and re-syncs to the backend, so a backend blip degrades to “syncs a few seconds later,” never “lost the answer.” Degradation spends the periphery to keep the core alive.

The load test is the proof, at the real shape

A steady “2× average” load test proves nothing here, because the failure is the curve, not the volume. The load test must reproduce near-zero to peak in ninety seconds against a production clone with the real project’s real quotas, and assert two things: p99 autosave latency stays under target, and zero writes are dropped. Tooling: a distributed Locust or k6 run (many workers, because one box cannot generate 1.4M synchronized virtual users), the connection ramp modelled as a step function rather than a gentle ramp, and a separate synthetic DDoS-plus-legitimate run so the war room learns to tell attack from herd on the dashboards before the day. Treat the load test as a required pipeline gate: an untested scale assumption is, on this platform, a court case in waiting.

The game-day runbook (T-minus timeline)

Exam day runs to a written runbook, not improvisation. A representative timeline:

When Action
T − 14 days Quota-increase requests approved (Compute instances, in-use IPs, Spanner nodes); change freeze begins; final full-shape load test passes.
T − 2 days Provision and warm the surge node pool; scale Spanner processing units up; size Memorystore; confirm war-room roster and bridge.
T − 1 day Dry run: verify the KEDA/scheduled trigger fires, Vault key-release timing, Datadog dashboards, and SLO burn alerts.
T − 3 hours Candidates begin connecting to the lobby (pre-connect the herd); pods at pre-warm floor; Cloud Armor in enforce mode; bridge open.
T − 30 min Confirm active-connection metric rising as expected; verify no capacity gaps; hard-freeze all changes.
T − 0 Vault releases the exam-paper key; release flag flips in Spanner; Pub/Sub fans out the start push. Watch concurrency, p99 autosave latency, Spanner commit latency.
T + 3 hours Exam ends; confirm every submission is durable in Spanner before scaling anything down.
T + 1 day Tear down the surge pool; scale Spanner and Memorystore back; blameless postmortem; per-window cost report to finance.

The runbook is the difference between a scheduled event and a gamble: every high-stakes moment is rehearsed, timed, and owned before it happens.

The shape of the win

For the testing authority, the payoff is not “a website that stayed up.” It is that on the Saturday in May, 1.4 million candidates pressed “Start” inside the same ninety seconds, the first question rendered in under a second, every autosave landed durably in Spanner the instant it was made, a volumetric attack at 09:01 was absorbed by Akamai and Cloud Armor without a candidate noticing, and at the end of the three hours not one answer was lost and not one candidate saw a spinner — so the result stood, unchallenged, and there was no re-sit. That last clause is what funds the platform. Everything upstream — the scheduled pre-warm, the surge node pool, Spanner’s external consistency, Memorystore’s shock absorption, the layered DDoS defense, Vault holding the paper key until the gun, the load test at the real shape, the Datadog war room — exists so that a candidate, a regulator, and a court each conclude the exam was fair. Start narrower if your problem is smaller, but for a country’s entrance exam on a single synchronized morning, this is where the architecture has to land.

Practice challenges

Work them in order — each builds a piece of the real design. Try before opening the solution.

1. Size the pre-warm floor (beginner). 1.4M candidates, each pod serves ~1,200 concurrent sessions. Because the start is synchronized, roughly how many pods must be already running at T-0, and what does that make your minReplicas?

<details> <summary>Solution</summary>

1,400,000 ÷ 1,200 ≈ 1,167 pods at peak. Because everyone arrives at once, the pre-warm floor must be near peak — set minReplicas to ~1,100–1,200 by T-0 (with maxReplicas a little higher for headroom), not a small fraction you plan to “scale up into.”

Why: a synchronized start gives you no time to scale, so the floor is the plan — under-set it and the herd beats your capacity. </details>

2. Sort the traffic (beginner). Label each as CDN-cacheable or must hit origin: (a) the exam shell HTML, (b) the JS/CSS bundle, © “fetch my next question,” (d) “autosave my answer,” (e) the printed candidate instructions PDF, (f) “sync my countdown timer.”

<details> <summary>Solution</summary>

Cacheable at the edge: a, b, e (identical for everyone, immutable). Must hit origin: c, d, f (per-candidate, dynamic, write-heavy).

Why: separating the static class from the dynamic class is the first scaling discipline — the CDN removes millions of reads so origin only sees the writes that actually need it. </details>

3. Throttle a scripted cheater (intermediate). Write a Cloud Armor rule that per-token (Authorization header) rate-limits the /api/v1/answer endpoint, banning a token that exceeds 100 requests/minute.

<details> <summary>Solution</summary>

gcloud compute security-policies rules create 1000 \
  --security-policy=exam-armor-policy \
  --expression="request.path.matches('/api/v1/answer')" \
  --action=rate-based-ban \
  --enforce-on-key=HTTP-HEADER \
  --enforce-on-key-name=Authorization \
  --rate-limit-threshold-count=100 \
  --rate-limit-threshold-interval-sec=60 \
  --ban-duration-sec=600 \
  --conform-action=allow \
  --exceed-action=deny-429

Why: keying on the token (not IP) throttles the account doing the scripting even behind shared/mobile NAT, while genuine candidates saving normally stay under the threshold. </details>

4. Pre-warm on a schedule (intermediate). On GKE, raise the exam-session floor to 1,000 replicas between 08:00 and 12:30 on Saturday, Asia/Kolkata, and let it fall back afterward. Sketch the mechanism.

<details> <summary>Solution</summary>

A KEDA cron ScaledObject (shown in Going deeper): start: "0 8 * * 6", end: "30 12 * * 6", timezone: Asia/Kolkata, desiredReplicas: "1000", with a low minReplicaCount off-peak. On a MIG instead: gcloud compute instance-groups managed update-autoscaling --set-schedule=... --schedule-cron="0 8 * * 6" --schedule-min-required-replicas=1000.

Why: the clock, not a metric, drives pre-warming — you raise a guaranteed floor before the spike and let reactive scaling only trim above it. </details>

5. Design the answers key to avoid hot-spotting (advanced). Your Spanner Answers table is written on every autosave. Propose a primary key that spreads writes across splits, and name the anti-pattern to avoid.

<details> <summary>Solution</summary>

Key on (SessionId, QuestionId) where SessionId is a UUID / hash-prefixed value, and interleave Answers under the Sessions table so a candidate’s rows co-locate on one split. Avoid a monotonically increasing key — a timestamp or sequential session id — which funnels every new write to the last split and caps throughput. See Spanner schema design & hot-spot avoidance.

Why: Spanner scales writes by splitting the key range; a monotonic key defeats splitting and serializes the whole write load onto one server. </details>

6. Build the degradation ladder (advanced). Capacity is 90% of the herd. List, in order, what you shed to stay up — and the one thing you never shed. Then say where the post-submit grading work goes.

<details> <summary>Solution</summary>

Shed in order: analytics/RUM sampling → widen autosave interval → serve questions from cache → disable non-essential UI → 429 + Retry-After on non-critical endpoints. Never shed the answer-save/submit path (the client is local-first and re-syncs). Post-submit grading goes on a queue — publish to Pub/Sub (or Cloud Tasks with a dispatch cap) and let workers drain it at a sustainable rate, so the durable save stays synchronous but the expensive work is leveled.

Why: graceful degradation protects the core (durable answer capture) by spending the periphery, and queue-based load leveling keeps the spike out of the latency-critical path. </details>

Common beginner mistakes

Glossary

GCPGKECloud SpannerAutoscalingEnterpriseEdTech
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments