- What data are we serving? Current conditions, hourly forecast, 10-day forecast, radar maps, air quality? โ All of these. Current conditions + hourly + daily forecasts are the core. Radar tiles and AQI are secondary.
- Do we run our own forecast models (NWP)? Or consume forecasts from government agencies (NWS, ECMWF)? โ We consume raw observations + model output from external sources (like NWS GFS/HRRR, ECMWF). We run a post-processing layer (bias correction, downscaling, blending) but not the physics simulation itself.
- Granularity? City-level, zipcode-level, or point-level (lat/lng)? โ Point-level at ~1km grid resolution. User sends lat/lng, we return the nearest grid point's data. This gives millions of unique "locations."
- Severe weather alerts? โ Yes โ push notifications for tornadoes, flash floods, hurricane warnings. These are life-safety critical and must be delivered within seconds.
- Scale? โ ~500M daily active users globally. Extreme read-heaviness. Weather data is highly cacheable โ it changes every 15-60 minutes, not every second.
- Monetization? โ Free tier with ads, premium tier with minute-level precipitation (hyper-local nowcasting), extended forecasts, no ads.
| In Scope | Out of Scope |
|---|---|
| Current conditions (temp, humidity, wind, UV) | Running NWP physics models (GFS, ECMWF) |
| Hourly forecast (48h) + daily forecast (14d) | Historical weather data warehouse |
| Radar / precipitation map tiles | Agricultural / aviation-specific products |
| Severe weather alerts (push notifications) | IoT device ingestion (personal weather stations) |
| Location search (city, zipcode, lat/lng) | Social features (user-reported conditions) |
| Air quality index (AQI) | Climate modeling / long-range seasonal forecasts |
- UC1: "What's the weather right now?" โ User opens app โ current conditions for their location in <200ms. Temp, feels-like, humidity, wind, UV index, condition icon.
- UC2: "Will it rain this afternoon?" โ Hourly forecast for next 48 hours. Precipitation probability per hour. Minute-level rain forecast for next 2 hours (premium).
- UC3: "10-day outlook" โ Daily high/low, condition summary, precipitation chance. Updated every 6 hours.
- UC4: "Show me the radar" โ Animated radar map tiles. Past 2 hours + 1 hour forecast. Pan/zoom map interaction.
- UC5: "Tornado warning!" โ Severe weather alert pushed to all affected users within 60 seconds of NWS issuance. Life-safety: zero message loss.
- Extreme read-heaviness (100,000:1 read:write): Weather data is produced by a handful of ingestion pipelines (writes) and consumed by 500M users (reads). This is the most read-heavy system you'll ever design. Caching is not an optimization โ it's the architecture.
- High cacheability: Current conditions update every 15 minutes. Hourly forecasts update every 1-6 hours. 10-day forecasts update every 6 hours. A response cached for 10 minutes is still fresh for 99.9% of requests.
- API response <200ms p95: Users expect instant weather. CDN cache hit should be <50ms. Cache miss (origin fetch) should be <200ms.
- Alert delivery <60 seconds: From NWS issuing a tornado warning to user seeing the push notification. This is the only latency-critical write path.
- Availability > consistency: Showing 15-minute-stale weather data is fine. Showing nothing (503 error) during a hurricane โ when people need weather most โ is catastrophic.
- Global reach: Users everywhere. CDN-first architecture. Data sources are mostly US/EU-centric (NWS, ECMWF), but serving is global.
| Requirement | Decision | Why (and what was rejected) | Consistency |
|---|---|---|---|
| 200K QPS peak, 95%+ identical responses | CDN-first (CloudFront/Fastly) | Weather responses are identical for every user in the same grid cell. CDN serves from edge, <50ms. Without CDN, origin would need 200K QPS capacity โ 20x overprovisioned for normal traffic. | AP |
| Forecast data fits in memory (~50GB) | Redis cluster for origin data | Entire forecast grid in-memory. O(1) lookup by grid cell ID. PostgreSQL would add 5-10ms disk I/O on cache miss โ unnecessary when data fits in RAM. | AP |
| Ingest data from 10+ external sources | Kafka pipeline (not direct polling) | Each source has different format, frequency, reliability. Kafka decouples ingestion from processing. If ECMWF is late, NWS data still flows. Direct polling creates tight coupling. | โ |
| Pre-render 10M location responses every 15min | Batch precomputation (Spark/Flink) | Compute all forecasts in bulk, write to Redis. On-demand computation at 200K QPS would overload the forecast models. Precompute = O(1) serving latency regardless of query complexity. | โ |
| Tornado alert to 10M users in <60s | Geo-targeted push via FCM/APNs | Push, not pull. Users can't poll for alerts โ they need immediate notification. Geo-fenced: only affected users. Fan-out via mobile push platforms (FCM handles delivery at scale). | CP |
| Radar tiles: interactive map with pan/zoom | Pre-rendered tile pyramid (z/x/y) | Standard slippy map tiles at zoom levels 2-12. Pre-rendered PNGs stored in S3, served via CDN. Dynamic rendering on each request would be 100x more expensive. | AP |
Data Collectors INGEST
- Poll NWS/ECMWF APIs on schedule (GRIB2, BUFR)
- Subscribe to METAR/SYNOP feeds (decoded text)
- Download NEXRAD Level-II radar data (binary)
- Normalize all sources to internal schema
Forecast Blender COMPUTE
- Model Output Statistics (MOS): bias-correct NWP output
- Downscale: 13km GFS grid โ 1km local grid
- Blend: weighted average of GFS, ECMWF, HRRR, NAM
- Output: best-estimate forecast per grid cell
Response Builder PRECOMPUTE
- Renders API-ready JSON per grid cell (current + forecast)
- Writes to Redis with grid_cell_id as key
- Runs every 15 min for current, every 6h for extended
- ~10M writes per cycle (parallelized across workers)
CDN + Weather API SERVE
- CDN edge caches responses by grid cell ID (15min TTL)
- Cache miss โ API server โ Redis lookup โ respond
- Lat/lng โ grid cell mapping: floor(lat ร 100), floor(lng ร 100)
- Cache key: /weather/{grid_id}?units=metric
Radar Tiler PRECOMPUTE
- Mosaics 160 NEXRAD sites into single composite
- Renders PNG tiles at zoom levels 2-12 (slippy map)
- Stores to S3 โ CDN. ~500K tiles per radar frame
- New frame every 5 minutes, ~12 frames for animation
Alert Processor ALERTS
- Subscribes to NWS CAP (Common Alerting Protocol) feed
- Parses alert polygon (lat/lng vertices)
- Queries geo-index: "which users are inside this polygon?"
- Fans out push notifications via FCM/APNs
| Source | Format | Update Frequency | Coverage | Resolution |
|---|---|---|---|---|
| GFS (NWS) | GRIB2 | Every 6 hours | Global | ~13km (0.25ยฐ) |
| HRRR (NWS) | GRIB2 | Every hour | CONUS only | ~3km |
| ECMWF IFS | GRIB2 | Every 12 hours | Global | ~9km |
| METAR | Text (decoded) | Every hour | Airports worldwide | Point observations |
| NEXRAD Radar | Binary Level-II | Every 5 minutes | 160 US sites | ~250m |
| GOES-16/18 | NetCDF | Every 5-15 minutes | Western Hemisphere | ~2km |
| Tier | Cache TTL | Hit Rate | Latency | What It Serves |
|---|---|---|---|---|
| CDN Edge | 15 min (current), 1h (forecast), 5min (radar) | 95%+ | <50ms | All users in the same grid cell get the same cached response. |
| Redis Cluster | Until next precompute cycle | 99.9% | <5ms | Pre-computed JSON per grid cell. Full forecast grid in memory. |
| Forecast Pipeline | N/A (writes to Redis) | N/A | Minutes | Batch computes all grid cells. Not on the serving path. |
grid:4071:-7401 (floor at 0.01ยฐ resolution โ 1km). Every user at Times Square hits the exact same cache key. This is why weather is so cacheable: the data is inherently spatial, and users cluster in the same cells. NYC has ~800 grid cells. Even if each is cached separately, that's 800 entries serving millions of users. Cache key: /v1/weather/{grid_id}?units={metric|imperial}&lang={en}. Units and language are part of the key because they change the response body.| Data | Store | Why This Store |
|---|---|---|
| Pre-computed forecasts | Redis Cluster (~50GB) | Entire forecast grid in-memory. O(1) lookup by grid cell ID. Sub-5ms reads on cache miss from CDN. |
| Radar tiles | S3 + CDN | ~500K PNG tiles per frame ร 12 frames = 6M objects. Immutable, content-addressed. CDN-native. |
| Raw observations | TimescaleDB | Time-series optimized. Auto-partitioned by time. Used for bias correction and quality control, not serving. |
| User profiles + locations | PostgreSQL + PostGIS | ACID for user accounts. PostGIS for geo-spatial queries (alert polygon containment). ~500M rows. |
| Location search | Elasticsearch | Fuzzy text search + geo-biased ranking. "New Y" โ "New York, NY" with distance-based boost. |
| Alert queue | Kafka (separate cluster) | Dedicated topic for alerts. Independent from forecast pipeline. At-least-once delivery guaranteed. |
- Hyper-local nowcasting (minute-level precipitation): ML model using radar + satellite to predict precipitation at 1-minute resolution for the next 2 hours. Dark Sky's killer feature. Requires real-time radar ingestion and GPU inference. Premium-only feature.
- AI weather foundation model (GenCast, GraphCast): Replace or augment traditional NWP with ML-based forecast models. Google's GenCast beats ECMWF on 15-day forecasts at a fraction of the compute cost. Train on decades of reanalysis data (ERA5). GPU inference instead of supercomputer simulation.
- Personalized weather intelligence: "It'll rain during your commute" โ combine user calendar, commute route, and minute-level precipitation. "Bad running weather this afternoon" โ learn user's outdoor activity patterns. Requires per-user context beyond just location.
- Crowd-sourced observations: Users report current conditions ("It's raining here"). Aggregate thousands of reports to improve real-time accuracy between official station observations. Apple Weather does this with iPhone barometer data.
- Climate adaptation dashboard: Historical trends: "This summer was 2.3ยฐC hotter than the 30-year average." Extreme weather frequency: "Your city had 15 more days above 35ยฐC than a decade ago." Enterprise version for agriculture, insurance, energy.
- Offline-first mobile app: Cache the next 48h forecast on-device. App works without connectivity โ critical during severe weather when cell networks are overloaded. Delta sync: only download changed grid cells.
Why precompute all forecasts instead of computing on demand with caching?
Three reasons: (1) Thundering herd elimination: when a CDN TTL expires during a hurricane, thousands of users hit the same cache key simultaneously. With on-demand computation, all of those requests cascade to your compute tier. With precomputation, they all hit Redis โ a simple GET, not a computation. (2) Predictable load: the precompute job runs on a fixed schedule (every 15 min) regardless of user traffic. Origin capacity planning is trivial. (3) Cold start: an on-demand system has no cached data when it first deploys or after a cache flush. With precomputation, Redis always has the full forecast grid โ there's no "cold" state. The tradeoff is 15-minute staleness, but weather changes on hourly timescales, so 15-minute-old data is effectively fresh.
How do you handle a tornado warning reaching 10 million users in under 60 seconds?
The alert path is completely independent from forecast serving. Step 1 (~5s): Alert processor receives NWS CAP XML, parses the alert polygon. Step 2 (~5s): Geo-index query finds all users with saved locations or last-known positions inside the polygon. Users are pre-indexed by geohash โ the query is a prefix scan, not a full table scan. Step 3 (~20s): Fan out push notifications via FCM (Android) and APNs (iOS). These platforms are built for millions of concurrent pushes. We batch 100K tokens per API call. 10M users รท 100K per batch = 100 API calls, parallelized across workers. The key design choice: push, not pull. Users can't poll for alerts โ their app might be closed. Push notifications wake the device and display immediately. For redundancy, we also update the in-app alert banner and send SMS to premium users.
Your CDN goes down during a hurricane โ the exact moment when traffic spikes 10x. What happens?
This is the nightmare scenario โ and it's a known single point of failure. Mitigations: (1) Multi-CDN: CloudFront primary, Fastly secondary. DNS-based failover. If CloudFront degrades, Route 53 health checks detect it and shift traffic to Fastly within 60 seconds. (2) Origin can't absorb full CDN traffic (200K QPS vs 10K QPS origin capacity), so if BOTH CDNs fail, we degrade gracefully: serve a simplified response (current conditions only, no hourly), shed low-priority endpoints (radar tiles, location search), and activate an emergency static page with regional weather summaries. (3) The alert path is independent โ push notifications don't go through the CDN at all, so tornado warnings still reach users even if the forecast API is degraded. The honest answer: if both CDNs fail simultaneously during peak, some users will see errors. But the life-safety alert path survives.
How do you handle the lat/lng to grid cell mapping efficiently at scale?
The mapping is pure math, no database lookup required: grid_lat = floor(lat ร 100), grid_lng = floor(lng ร 100). This snaps any lat/lng to the nearest 0.01ยฐ grid cell (~1.1km at the equator). The cache key becomes deterministic: grid:{grid_lat}:{grid_lng}. This means the CDN can cache by URL path โ no query-string normalization needed. The 0.01ยฐ resolution gives ~10M unique land-surface grid cells. At the equator, 0.01ยฐ โ 1.1km. At 60ยฐ latitude, it's ~550m (longitude cells shrink with latitude). For higher fidelity near population centers, we could use a variable-resolution grid (finer in cities, coarser in oceans), but the uniform grid is simpler and 1km is sufficient for consumer weather. The key property: every user at the same 1km patch gets the identical response, making CDN caching trivially effective.
How do you make the forecast more accurate than just serving raw NWS data?
Three techniques compound accuracy: (1) Model blending: GFS, ECMWF, HRRR, and NAM each have different strengths. We maintain a rolling accuracy score per model per region (comparing past forecasts to actual observations). The blender assigns weights proportional to recent accuracy โ if ECMWF has been beating GFS in the Pacific Northwest this week, it gets higher weight there. (2) Statistical downscaling (MOS โ Model Output Statistics): NWP models output at 9-13km resolution and have systematic biases ("GFS is always 2ยฐC warm in Denver in January"). We train regression models that correct these biases using years of forecast-vs-observation pairs. (3) Observation assimilation: between NWP model runs (every 6-12 hours), we blend real-time station observations to keep "current conditions" fresh. If a METAR station reports 28ยฐC but the GFS forecast said 25ยฐC, we adjust the nearby grid cells toward the observation. This is the secret sauce that differentiates weather apps โ everyone gets the same raw NWP data, but the post-processing is proprietary.