The Daily Quiz has no daily content, and that is deliberate
Daily Quiz seeds nothing by date and generates nothing when the app opens. The quizzes are the output of a durable background queue, and the only daily thing about the feature is which five of them we remind you about.
The feature is called Daily Quiz. Almost nothing about it is daily. There is no set of questions seeded for today, nothing generated when the app opens, no server row keyed by the date, and no branch or subject targeting. The list is the accumulated output of a background job queue, and the only thing that changes from one day to the next is which five of those quizzes we remind you about.
Where the quizzes actually come from
Ticking a topic in Update Learning does three things. It records the topic as complete, it seeds a revision item, and it queues generation. The queue is the interesting part, and its ordering is the whole design:
// Persist the pending entry before any network call, so that a
// crash or a network drop cannot lose it.
await _pendingStore.add(PendingGeneration(...));
Only then does it try. Flashcards are generated first, then the quiz, sequentially, and the pending entry is removed only when both exist. If either fails, the entry stays and is retried later. Partial progress is better than all-or-nothing, and a retry re-checks what is missing instead of redoing everything.
The retry pass runs a reachability probe first and skips the whole pass when the device is offline, so an offline hour does not burn down the attempt counter. After five attempts the item is marked failed and surfaces on the empty state with a manual Retry button, rather than retrying forever in the background.
Why not generate today's quiz when the app opens
Because that puts a language model call on the cold start path. It cannot run offline, which is exactly when a student most wants to revise. It turns every app open into a latency spike. And one rate limit, one spent daily quota, one flaky connection, and the student has no quiz for the day and nothing to fall back on.
Local-first inverts that. Generate once, store it, play it offline, forever. The price is staleness, and it is a real price: the queue short-circuits when a quiz already exists for that topic, so the questions generated in September are the questions you sit in December. The only way to get fresh questions for a topic is to swipe the quiz away and tick the topic again.
The daily part is the reminder
What is daily is the notification rotation. The scheduler takes the unplayed quizzes, sorts them oldest first, and passes them through a sliding window:
offset = (dayIndex * cap) % candidates.length;
Five per day, advancing with the date. A backlog of twenty quizzes therefore surfaces completely over four days instead of the newest five starving everything behind them. Sorting oldest first is a product decision with its reason written down: a quiz generated a week ago is the one at risk of being forgotten.
The notification carries quiz:<deckKey>, which parses back into the route plus that specific deck, so tapping it opens the quiz rather than a list you then have to search.
The comment that says exponential backoff, and the code that does not
The class documentation says a retry timer with exponential backoff keeps re-running pending topics. There is no exponential backoff. The delay is initialised to one minute, and every assignment to it in the file resets it to that same constant — it is never multiplied. Retries run at a flat one-minute cadence, bounded only by the five-attempt cap.
Flat retry is not obviously wrong for a backlog this small, and a real backoff would mostly add delay to a queue that is usually empty. The problem is the comment. A comment describing code that does not exist is worse than no comment, because the next person reads it instead of the code.
The timeout that does not cancel anything
There is a two-minute generation guard, and it is advisory. When it fires, it removes the key from the in-progress set, clears the start time, and reschedules the topic. It does not cancel the request already in flight, and the service's own worst case is longer than two minutes: sixty seconds per HTTP attempt, across providers and across each provider's keys.
So when the retry pass runs and finds that no quiz exists yet for that topic, a second generation can start alongside the first. We are describing what the code permits, not something we have watched happen — we have no measurement of how often the two windows overlap, and there is no test for the timer at all.
What actually bit us in production
None of the above. The bug students reported was simpler: tick two topics quickly and one of the quizzes silently never appeared. Two concurrent generations hit the provider's rate limit and the loser was dropped. The fix was to serialise generation inside this provider so only one runs at a time, which is an older and narrower fix than the app-wide work queue that came later. Both still exist, and the narrow one earns its place because the contention it prevents starts before the app-wide queue is involved.
Why not generate fresh questions each day?
Because the content is per-student, derived from the topics they ticked in their own syllabus, and a daily refresh would spend quota replacing questions they may not have finished yet. It would also need the network at exactly the moment the current design does not.
The real problem is the name. A feature whose content never changes should not be called Daily anything, and the honest description is "quizzes for the topics you have finished, with a rotating nudge". We would rather fix the name than defend it.
Keep reading
Related reading
We store no record of which dose you saw, on purpose
The Home card and the notification both need to know which item is today's. One of them runs in a background isolate that cannot open the database at all — so instead of syncing a cursor, we deleted it.
A flashcard that always turns up, even on a train
Tick twenty topics on a bad connection, close the app, and it still finishes the job. Here is the retry queue that made AI generation durable.
The card you mark as known never comes back
Flashcards has three states and no spaced-repetition scheduler. Marking a card known removes it from revision permanently, and four different screens disagree about which cards are due.