How it is built
Architecture overview
A spaced-repetition system keeps a log of every time a learner answered an item, turns that log into an estimate of how likely the learner is to still remember the item, and shows each item again just before the estimate falls below a target (for example 90 %). Four things make up the system: a store of items and the review log, a memory model that predicts recall for one item at one moment, a scheduler that turns the prediction into a due date and builds today's queue, and a trainer that fits the model's parameters to the review log. Around them sit sync (the same collection on phone and laptop) and experiments (does the new model actually help?). The one idea that makes it work: the interval is not a fixed ladder (1 day, 3 days, 7 days) but a decision from a forgetting curve, and the curve is learned from the learner's own history. Two well-documented designs share this skeleton: Anki, where the collection and the scheduler live on the user's device and the parameters are fitted per user; and Duolingo's half-life regression, where one model is fitted on all students and run by a server.
Parts:
- Review store — items, cards and the append-only review log on the device or server — B+ tree, Write-ahead log
- Memory model — recall probability and strength of one item —
spaced-repetition(no page yet), Item response theory, Linear and logistic regression - Scheduler and queue — due dates, today's queue, day boundary — d-ary heap,
calendar-date-arithmetic(no page yet) - Trainer — fits the model's parameters to the review log — Linear and logistic regression, L-BFGS
- Sync — one collection on several devices — Incremental sync cursors, Offline mutation queue
- Experiments — compare schedulers on real learners — A/B testing
learner ──answer (item, grade, time)──► review UI
│ 1 row per answer
▼
┌───────────── store (SQLite / DB: WAL + B+ tree indexes) ──────────────┐
│ items/cards (state, due, memory state) review log (append-only) │
└──────▲───────────────────────┬──────────────────────────┬─────────────┘
│ new state + due │ due ≤ now │ whole log
┌──────┴───────┐ ┌──────▼───────┐ ┌──────▼───────┐
│ memory model │◄params─┤ scheduler: │ │ trainer │
│ (R, S, D or │ │ queue, limits│ │ (offline fit)│──► new params
│ half-life) │ │ day rollover │ └──────────────┘
└──────────────┘ └──────────────┘
▲ sync: changes after a cursor ▼ experiments: arm per learner
device A ◄──────────────────────► server ◄──────────────────► device B
How the parts fit together
Memory model
The model answers one question: given what happened to this item before and how long ago, what is the chance the
learner recalls it now? The block spaced-repetition (no page yet) is meant to cover the family; here is how the documented
members differ.
- SM-2 (SuperMemo, 1990) is a rule, not a fit: after the n-th successful repetition the interval is
I(1)=1,I(2)=6,I(n)=I(n-1)·EF, and the easiness factorEFis updated from the answer qualityq(EF' = EF − 0.8 + 0.28q − 0.02q²), never below 1.3 (source: the SM-2 description at super-memory.com). Cheap and predictable, but it has no notion of "how likely is recall right now", so a card reviewed late is treated like one reviewed on time. - Half-life regression (HLR) (Settles and Meeder, ACL 2016) predicts recall as
p = 2^(−Δ/h), whereΔis the time since the last practice and the half-lifeh = 2^(θ·x)is a learned linear function of features: counts of times seen, correct and wrong, plus an indicator per lexeme tag (about 20 thousand of them), which acts as a per-item difficulty. The paper boundshto between 15 minutes and 9 months. - FSRS (Free Spaced Repetition Scheduler) keeps three numbers per card: retrievability R (chance of recall now), stability S (days for R to fall from 100 % to 90 %) and difficulty D (how hard it is to raise S); the current version, FSRS-6, has 21 parameters (source: the FSRS "ABC" wiki). Anki ships it since 23.10.
Where Item response theory fits: both HLR's per-lexeme weight and FSRS's D are item difficulty, and a learner's overall parameter fit is ability. IRT is the older, cross-sectional version of the same two-sided idea, and is the right tool when you also want to place new items by difficulty before anyone has reviewed them (reasoning, not a claim about either product). The fitting of HLR is Linear and logistic regression in spirit: a smooth loss over a linear predictor, trained on log data.
Contract with the scheduler: the model returns (recall probability at time t) and (the time at which recall reaches the target). The scheduler never looks inside.
Scheduler and queue
The scheduler turns the model's output into a due date, then into a queue. The levers a product exposes are few and worth copying:
- Desired retention. In Anki, the most important FSRS setting; the default is 90 %. The manual notes that workload rises quickly above 90 % and can become overwhelming above 97 %.
- Short steps before the first long interval. Anki recommends learning and relearning steps shorter than one day when FSRS is on (for example 10 minutes), so a step never straddles the day boundary.
- Daily limits and sibling burying. New cards per day and review cards per day cap the load; "bury siblings" delays other cards from the same note until the next day (Anki manual).
- Load balancing and fuzz. Anki's scheduler spreads due dates so that a big batch learned on one day does not
come back as one big batch; the code constrains the choice to a window and caches workload per day for up to
90 days of interval plus 10 % (source:
load_balancer.rsin the Anki repo).
For the queue itself, a single learner's due list is small enough for a plain index query. The d-ary heap matters where a service ranks many candidates by predicted recall (pick the items with lowest R for this learner's next session, as Duolingo does when ranking target words for a practice session) because a bounded heap gives the top k without sorting the rest. That is a design choice, not something Anki does.
The day boundary is a real contract, and calendar-date-arithmetic (no page yet) is the missing block that
describes it. Anki counts "days elapsed" from the collection's creation and moves to the next day at a configurable
rollover hour (for example 4 a.m.), so someone studying at 1 a.m. is still on yesterday's day; a step that crosses
the boundary becomes a whole number of days (source: timing.rs in the Anki repo; the manual's example converts a
6-hour delay into 1 day when the next day starts in 5 hours). Time zones and daylight saving make "one day later"
ambiguous, so store absolute timestamps for steps under a day and day numbers for the rest.
Review store
Both products keep the data in a relational store, and the review log is the one thing you must never lose: it is
the training set for the model and the audit trail of the scheduler. Anki's collection is SQLite, opened with
journal_mode = wal and exclusive locking (source: storage/sqlite.rs), which is the
Write-ahead log contract: a crash during a review leaves either the old or the new state, never
half. Lookups ("cards due today in this deck") hit B+ tree indexes on due date and card state.
Ownership: the review UI is the only writer; the scheduler writes the new card state and appends one log row in the
same transaction, so card state and log can never disagree.
Trainer
Training fits model parameters to the log. HLR is fitted with AdaGrad (an online gradient method) on a smooth loss
(p − p̂)² + α(h − ĥ)² + λ‖θ‖² (source: the HLR paper). The released data set holds 13 million Duolingo learning
traces (source: the HLR repository README). Anki's FSRS "Optimize" button fits the parameters to the user's own
review history and needs data to learn from: the manual names fewer than a few hundred reviews as a common reason for poor
fits. L-BFGS is the natural solver when the fit is small, dense and smooth (a few dozen
parameters, full-batch), while AdaGrad-style online methods suit very large sparse feature sets; neither product
page above claims L-BFGS, it is the textbook choice for the second case.
Contract between trainer and scheduler: parameters are a versioned, small vector. A change of parameters can re-compute the due dates of existing cards from their history. Anki makes this an option, "Reschedule cards on change", which is off by default: future reviews use the new scheduling but today's workload does not jump. The manual does not recommend switching it on when first moving from SM-2, because many cards can become due at once, and suggests a backup first.
Sync
A learner reviews on a phone on the train and a laptop at home. Incremental sync cursors
is the block that keeps this cheap: every row carries an update sequence number (USN), the client remembers the last
USN it saw, and a sync sends only rows changed after it. Anki's Rust sync code shows the pieces: pending_usn,
server_usn, chunked cards, notes and review-log rows, a finish step that increments the server USN (source:
rslib/src/sync/collection/*.rs). Rows deleted since the last sync travel as "graves". The server can be
self-hosted (Anki manual, "Self-Hosted Sync Server").
Writes made offline go through the pattern of Offline mutation queue: apply locally at once, send later. The review log makes this safe, because each review is an append: two devices that both reviewed the same card produce two log rows, and the card's state can be recomputed from the merged log.
Experiments
"Is the new scheduler better?" is an A/B test, and it is harder than usual because the outcome (what the learner remembers) shows up weeks later and the learner's behaviour changes with the scheduler. Duolingo assigned students at random to HLR or to the Leitner system it used before; the experiment ran six weeks with just under 1 million students (source: HLR paper). The paper reports retention per activity type for two experiments, and the results are mixed: not every activity type improved, and not every difference was significant. Read the paper's results table, not only the abstract.
Data and control flow
One review (device):
- The queue gives the next due card; the UI shows the question and starts a timer.
- The learner grades the answer (Anki: Again, Hard, Good, Easy). Anki's manual warns against pressing "Hard" for a failed recall, because FSRS then assumes the answer was correct.
- The memory model updates the card's state from the grade and the elapsed time and returns the next due time.
- One transaction writes the new card state and appends a review-log row (WAL commit). Time here is microseconds to milliseconds (order of); nothing waits for the network.
- The queue shows the next card. Sync runs later, in the background.
Building today's queue:
- At the rollover hour the day counter increases; cards with due day ≤ today become due.
- Limits apply: new cards per day, reviews per day, burying of siblings.
- Ordering rules decide the sequence (FSRS offers "ascending retrievability", which shows the cards most likely forgotten first).
One training cycle (nightly, or on demand):
- Read the review log (the whole history for one learner, or a sample across learners).
- Fit the parameters; compare the new fit against the old on held-out reviews.
- If better, publish the parameter version; reschedule or apply to future reviews only.
- Sync carries the new parameters to other devices as part of the preferences.
Scaling
In the order you would pull the levers:
- A single learner is tiny. Thousands of cards and tens of thousands of log rows fit in memory; the cost is interface latency, not compute. Keep the review path to one local transaction.
- Sync size grows with changed rows, not with the collection, as long as every table has a USN. The first sync and the "full upload" fallback are the expensive paths; chunk them.
- Trainer cost grows with log length. Fit per learner when logs are long enough; otherwise fall back to default parameters trained on a pooled data set (the role of the 13-million-trace data set in HLR).
- Many learners on one service. Move the memory model behind a server, precompute features (counts per item) incrementally rather than from the raw log, and rank candidates with a heap. The paper bounds HLR's inputs to features "available in the production system without adding latency".
- Workload spikes. Load balancing and desired retention are the controls: the Anki manual warns that workload rises quickly above 90 % retention and can become overwhelming above 97 %, so the target, not the code, sets the daily load.
Failure modes
| Failure | Symptom | Cause | Fix |
|---|---|---|---|
| Review pile-up | hundreds of due cards after a break, the learner quits | high desired retention, no daily cap, scheduler treats late reviews as on time | cap reviews per day, use a model that handles delay (retrievability-aware), allow postponing |
| Day-boundary bug | cards due "tomorrow" appear twice, or vanish for travelers | local midnight vs rollover hour, DST, time-zone change | store UTC instants for short steps, day numbers from a fixed epoch plus rollover hour |
| "Hard" misuse | intervals far too long for forgotten cards | failure graded as Hard, model reads it as a recall | label the buttons clearly, document it (Anki manual does) |
| Overfit optimizer | wild intervals right after Optimize | too few reviews for the parameters | keep defaults below a minimum count, regularize toward defaults |
| Sync conflict | review lost, or a card duplicated | two devices changed the same row, last writer wins | append-only log, recompute card state from merged log |
| Log loss | scheduler cannot be retrained | log row written outside the card-state transaction, or pruned | one transaction for both, never delete log rows |
| Misleading experiment | headline gain does not hold | different metric in different arms, novelty effect | pre-register the metric, report per-experiment tables, run for weeks |
Real implementations
| name | what it is | repo | licence | notes |
|---|---|---|---|---|
| Anki | desktop and sync-server spaced-repetition app, SQLite collection, Rust core | ankitects/anki | AGPL-3.0-or-later (with BSD-3 parts) | scheduler, sync, FSRS integration in rslib/ |
| fsrs-rs | Rust implementation of FSRS: optimizer, scheduling, simulation | open-spaced-repetition/fsrs-rs | BSD-3-Clause | used by Anki's scheduler (use fsrs::NextStates) |
| fsrs4anki | the original FSRS project and wiki | open-spaced-repetition/fsrs4anki | MIT | algorithm description and optimizer in Python |
| py-fsrs / ts-fsrs | FSRS schedulers for Python and TypeScript apps | py-fsrs, ts-fsrs | MIT | for building your own app around FSRS |
| halflife-regression | Duolingo's released HLR code and 13M-trace data set | duolingo/halflife-regression | MIT | Python implementation and baselines from the paper |
| Mnemosyne | open-source flashcard program with its own sync protocol | mnemosyne-proj/mnemosyne | mixed (LGPL-3 for sync, see repo) | a second, independent design |
Duolingo's production system is not open source; only the model, code and data of the 2016 paper are public.
Sources
- B. Settles, B. Meeder, A Trainable Spaced Repetition Model for Language Learning, ACL 2016, pages 1848-1858. paper · code and data
- P. Wozniak, Application of a computer to improve the efficiency of learning (SM-2 description, 1990). super-memory.com
- Open Spaced Repetition, ABC of FSRS (memory state R, S, D; 21 parameters in FSRS-6). wiki
- Anki manual: deck options and FSRS, self-hosted sync server.
- Anki source:
rslib/src/storage/sqlite.rs(WAL),rslib/src/scheduler/timing.rs(rollover),rslib/src/scheduler/states/load_balancer.rs,rslib/src/sync/collection/(USN, chunks), in ankitects/anki.