Updated August 8, 2026
Why we removed the daily review cap
Version 1.8.1 of Moji - Learn Japanese Daily for iPhone, shipped in August 2026, removed the daily review cap. Before that, a study session served a small batch of reviews, quietly deferred the rest, and offered a “Keep reviewing” button for another chunk. It looked like a kindness. It was really a second scheduler sitting on top of FSRS, making decisions the first scheduler had already made better, and hiding the evidence. This is the post-mortem: what the cap did, how it starved the study loop at exactly the moment a spaced repetition app has to work, what the replacement looks like in code, and what I am watching now.
What the review cap actually did
The cap shipped in version 1.6.0, a few weeks earlier. Each session computed a review budget from the daily new-card goal: twice your goal, clamped between 10 and 20 reviews. If more cards were due, the session triaged them, kept the ones with the lowest retrievability, and deferred the rest to a later batch.
That triage rule was the part I was proudest of, and it is the part that turned out to be exactly backwards. Retrievability is FSRS’s estimate of the probability you would recall a card right now, and Moji renders it as memory bands. Keeping the lowest-retrievability cards means keeping the ones you have most likely already forgotten, which is what makes “the rest can wait” technically true. The deferred cards really were the safest ones.
The problem is that safety was measured against a session you were going to finish. If you did not finish, the cap had spent your attention on the cards least likely to be rescued.
Why capping daily reviews looks like the kind choice
The argument for a cap is genuinely good, and I still think it is good. Spaced repetition punishes absence. Miss a week and the queue does not wait politely, it accumulates, and the number you come back to is larger than the number that made you leave. A wall of due cards is the classic reason people quit an SRS, and “you owe 300 reviews” is a worse greeting than “here are 20.”
So the cap was not a lazy decision. It was a decision to protect the returning learner from a number. What I did not appreciate is that protecting someone from a number is not the same as protecting them from the work, and that the number was still visible everywhere else in the app.
How the cap starved the loop at cold re-entry
The failure was not mid-session. Once someone started swiping, sessions drained fine. It was at cold re-entry: opening the app after a break, which is exactly the moment the whole product either works or does not. Three things went wrong there, and none of them showed up as a bug report.
The session and the rest of the app disagreed. The home-screen widget, the Due tile, and reminder copy all counted the true due number, because none of them knew about triage. Only the session applied the cap. So the app could tell you 60 cards were waiting, hand you 20, and then show the done screen. A learner returning after a break was served a puzzlingly small queue and told they were finished, with no way to tell whether the app was being generous or broken.
“Done” stopped meaning done. The session-completed signal fired on the first drain of a capped batch, and continuations after that never re-fired it. The app’s internal notion of a completed day and the learner’s notion of a finished backlog had come apart, which also meant everything hanging off completion (the streak banner, the withholding of new cards, reminder scheduling) was hanging off a redefined word.
Deferral hid the backlog from the person carrying it. A session showing a comfortable handful of due cards could be sitting on a much larger deferred pile, and nothing in the interface said so, because saying so would have defeated the point. The state that most needed a nudge looked, from inside the app, like the healthiest state there is.
Put together: the cap solved the discouraging-number problem by lying, and the lie broke the feedback that makes an SRS loop close.
The fix: full queue, hardest-to-lose-first
The replacement (PR #77, 14 commits, net deletion) is close to the Anki model, deliberately, with no migration. Four decisions carry it.
Serve the full due queue. No budget, no triage, no deferred count, no “Keep reviewing” button. If it is due, it is in the session. Every surface that counts due cards now agrees with the session by construction, because there is only one number left to disagree about.
Sort by descending retrievability. This inverts the old triage. Still-rememberable cards come first, so an abandoned session rescued what it could, and the likely-forgotten cards lose very little from a bit more delay. Cards too young to have a retrievability at all (fewer than two reps) lead outright, since they are the least established of the lot. Because retrievability is an FSRS evaluation rather than a stored field, the queue is built with a decorate-sort: compute it once per card, then sort, rather than re-evaluating inside every comparison.
No finished-day freeze. An earlier version of the fix held cards that came due later the same day until tomorrow, to keep “done” feeling done. That got reverted during review: it silently skipped the 10-minute learning steps and pushed every day’s new cards onto the long-term stability branch. Due now means served, whenever it comes due.
A 20-minute learn-ahead window, consulted only at the end. When the queue would otherwise empty, the session looks ahead 20 minutes for learning steps that are nearly ready, so a second pass flows instead of flickering the done screen. Two rules guard it: a card must have rested at least 60 real seconds since its last rating, and at least half its scheduled step must have elapsed. If nothing qualifies, the done screen counts down and resumes on its own.
One consequence I accepted rather than fixed: the “N remaining” label counts what is in the queue right now, so failing a card makes it tick back up. Monotonic descent would be prettier, and it would also be a lie. The bouncing number is the honest reading of an Again.
Why I did not forgive anyone’s backlog
The obvious kindness on removal day is a one-time migration that reschedules everyone’s overdue cards so nobody opens the app to a wall. I wrote the plan and then threw it away.
Two reasons. First, in the Anki ecosystem this is always an explicit user action, never something the scheduler does behind your back, and I think that instinct is right: a backlog is information about your own study history, and quietly deleting it teaches nothing. Second, FSRS does not need the help. Passing a badly overdue card leaps its stability forward, because surviving a long gap is strong evidence the memory is stronger than the model assumed. Backlogs drain faster than one-card-per-slot arithmetic suggests, so the migration would only have hidden the starting point.
The accepted cost, stated plainly: anyone carrying a deferred pile under the cap saw its real size once, on update day, and future lapses show an honest count instead of a curated one. New cards now queue behind a backlog, which is normal SRS discipline and was the pre-cap behavior too.
The bug the rewrite uncovered
Rebuilding the session forced me to re-read the reminder logic, and there was a real bug in it. Same-day reminders had a recency gate meant to avoid nagging someone who had just been in the app, and the gate measured recency from the moment scheduling ran rather than from the moment the reminder would fire. Scheduling only ever runs around app activity, so almost every same-day reminder was measured against a foreground event seconds old and suppressed at birth. The feature was live and effectively silent.
Both gates now judge the fire time, and the study gate scales with what is left: a review today silences the day’s remaining reminders only when little remains due, so someone who did three cards out of sixty still gets the nudge. One hole is still open, where a full drain cancels the routine and nothing is scheduled for future days until the next app open.
Traps worth writing down
Three things bit me during the build, all of them the kind of bug that survives per-commit review.
A SwiftUI instance-reuse trap: the card stack keys views by word id, so re-serving the same card into the front slot reuses the same view with its swipe offset still parked off-screen, which renders as a blank screen. The card view now resets its fly-out state after a swipe.
A date-boundary trap: review cards due tomorrow all carry midnight exactly, so any “next 20 minutes” window evaluated near midnight sweeps in the entire next day. The learn-ahead fetch has to exclude review-state cards in the predicate, not afterwards.
And the one I would tell anyone building a similar queue. My first fix for re-serving a just-failed card was identity-based: skip the last card, then the last two. That works until the pool of resting cards is smaller than the exclusion ring, at which point three cards on short steps cycle forever at swipe cadence. Any identity ring caps spacing at ring-size swipes. The rule has to be time-based, and it is: learn-ahead skips anything rated in the last 60 seconds.
What I am watching now
The cap removal is live and, as of early August 2026, carries most sessions. I am deliberately not calling a verdict yet. What I am tracking, qualitatively: whether repeated failures resolve into real spaced rounds instead of a loop, whether people who park at a countdown come back or leave, whether the newly-firing reminders raise opt-outs, and how the heaviest backlogs behave over weeks rather than days. An honest read needs time, and the temptation to declare victory on a week of data is exactly how the cap got shipped in the first place.
The broader lesson: if you add a policy layer on top of a scheduler, every surface that reports state has to know about that layer, and the learner has to be able to see what it did. A cap you can see is a setting. A cap you cannot see is a bug with good intentions.
If you want to see what a no-cap FSRS-6 loop feels like in practice, Moji is on the App Store. It is iPhone-only, it runs FSRS-6 rather than SM-2, and the free tier covers the whole comprehension path: flashcards, mnemonics, and example sentences. There is more on how the scheduler works, how memory bands are drawn, what moving from Anki looks like, and how long the road actually is. Moji is on the App Store.