Duolingo
Duolingo: serving streaks to 100+ million learners on DynamoDB
One row per learner per course, tens of billions of items, and a cache TTL change that erased 2.1 billion API calls a day.
Why a key-value store runs the classroom
Duolingo's hot path is brutally simple and brutally frequent. Every time a learner opens the app, the backend must fetch that one user's state: their streak, their XP, their progress in every course they are enrolled in. It never needs to join that data with anything else on the read path, and it needs the answer fast for over 128 million monthly active users hitting more than 500 backend services (per Duolingo's platform team at InfoQ Dev Summit Munich 2025). That shape, single-user lookups at enormous volume with no relational queries in the request path, is exactly what Amazon DynamoDB is built for, and AWS has long featured Duolingo as a marquee customer, at one point profiling them storing 31 billion items in the service.
The interesting part is how the data is modeled. DynamoDB rewards access-pattern-first design: you decide what questions the application will ask, then shape items so each question is one key-value operation. Duolingo's engineering blog gives a concrete example from Birdbrain, the personalization system that scores every learner in every course. Rather than normalizing scores into many records, 'the scores for each course a learner is enrolled in is stored as a single row in Dynamo to minimize our API latency.' One GetItem returns everything the session needs. Updates are buffered in memory per user with an LRU eviction policy so the service is not hammering the table with a write per answer, and the event streams feeding those models are sharded by userId so all of one learner's activity lands on the same processor.
Compare that to the relational instinct: a users table, a courses table, an enrollments join table, and a query with two joins on every app open. At Duolingo's request volume that design buys you nothing (the app never asks relational questions at read time) and costs you latency and money. The lesson generation engine follows the same philosophy of precomputation: course content is processed offline, serialized to S3, and cached in memory by the serving fleet, so the request path touches only fast, pre-shaped data.
The request path, step by step
Follow a lesson request through the documented stack. The mobile client calls Duolingo's API layer, which is a fleet of microservices that historically ran on Amazon ECS ('for the mass majority, they're on ECS' per Duolingo's platform team at InfoQ) and is now migrating to EKS. The service that owns the learner's data checks a cache first. Duolingo runs Amazon ElastiCache clusters and in-process caches, and treats cache policy as a first-class tuning knob: their cost engineering post describes raising one internal cache's TTL from 1 minute to 1 hour, which cut traffic to the downstream service by more than 60 percent, and a related refactor that eliminated 2.1 billion unnecessary API calls per day.
On a cache miss, the service issues a key-value read to DynamoDB, keyed by user, and gets back the single row holding that learner's per-course state. Because items are shaped around the access pattern, there is no scatter-gather and no fan-out query. How much this caching layer matters showed up when Duolingo built request tracing across their microservices: tracing revealed 'a very common internal method was querying Dynamo multiple times for essentially the same data, rather than caching.' Caching that one method cut Duolingo's overall latency by 10 percent and reduced cost at the same time, because in DynamoDB every avoided read is capacity you do not pay for.
The same table-plus-staging pattern shows up at the extreme end of their traffic. For a Super Bowl ad campaign, Duolingo needed to deliver 4 million push notifications within about 5 seconds of the ad airing. The campaign's target user lists lived in DynamoDB, but the team did not hammer the table at air time: they prefetched the lists into S3 ahead of the game, staged work through FIFO SQS queues (whose 5-minute deduplication window guards against sending anyone a duplicate notification), and ran the workers on a dedicated ECS cluster. 99 percent of notifications arrived within 5.7 seconds. Reliability engineering wraps the whole path: when learners hit a degraded mode during an incident, an internal tool the team calls Freeze Gun tracks those users and makes sure their streak is preserved, because the streak is the product promise the architecture exists to keep.
What this teaches for the exam
Task 3.3 asks you to determine high-performing database solutions, and Duolingo is the canonical decision pattern. When a workload is key-value lookups by a known key, needs consistent single-digit-millisecond latency, and must scale horizontally to millions of users, the answer is DynamoDB, not a bigger RDS instance. Expect questions that describe exactly this shape (user profiles, game state, session data, leaderboards) and reward you for noticing that no relational features are used on the hot path. Remember the modeling corollary: DynamoDB performs when items are designed around access patterns, so 'store it as one item and read it with one GetItem' beats normalization. And know the caching ladder in front of any database: in-process caches, ElastiCache (Redis or Memcached) as a cache-aside layer, and DAX when you want a transparent, DynamoDB-specific read-through cache without changing application logic. Duolingo's 10 percent global latency win from caching one chatty method is the real-world version of the exam's favorite claim that caching relieves database read pressure.
Task 4.3 asks for cost-optimized database solutions, and every lever Duolingo pulled in their 20 percent cloud savings effort maps to an exam answer. They added missing TTL rules to DynamoDB tables that had accumulated stale data: DynamoDB TTL deletes expired items at no cost, which shrinks storage and is the exam's preferred answer for expiring session or ephemeral data. They tuned cache TTLs to slash downstream request volume: fewer reads means fewer consumed read capacity units, so caching is a cost control, not just a performance one. They switched a relational database to Aurora I/O-Optimized and saved several hundred thousand dollars a year: know that I/O-Optimized trades a higher instance/storage rate for zero per-I/O charges and wins when I/O is a large share of the bill. Round this out with the capacity mode decision the exam loves: on-demand for spiky or unknown traffic, provisioned (with auto scaling) for steady, predictable load where reserved throughput is cheaper. If a question pairs a read-heavy DynamoDB table with rising costs, look for the answer that adds a cache or a TTL before the one that adds capacity.
More diagrams
Sources
- Duolingo Engineering: Inside Engineering at Duolingo (single-row course scores in DynamoDB, LRU write buffering, userId-sharded streams)
- Duolingo Engineering: How we reduced our cloud spending by 20% (DynamoDB TTL rules, cache TTL tuning, Aurora I/O-Optimized)
- Duolingo Engineering: Improving the Duolingo experience with request tracing (repeated Dynamo queries, 10% latency reduction from caching)
- InfoQ / QCon London: How We Created a High-Scale Notification System at Duolingo (DynamoDB campaign lists, S3 prefetch, SQS FIFO, Freeze Gun)
- InfoQ Dev Summit Munich: Duolingo's Kubernetes Leap (128M+ MAU, 500+ backend services, ECS to EKS)
- AWS (video): Duolingo Stores 31 Billion Items on Amazon DynamoDB and Uses AWS to Deliver Language Lessons
- Duolingo Engineering: Rewriting Duolingo's engine in Scala (offline course data in S3, in-memory caching, Elastic Beanstalk)
Practice what you just read