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 Update Learning screen is a checklist of what you have covered. Tick a topic and the app generates a flashcard deck and a quiz for it.
The original version did the obvious thing: when you ticked a topic, it called the AI and saved the result. On wifi this is instant and feels like magic.
On a train it is a small disaster.
What actually goes wrong
Someone revising on a journey ticks twenty topics as they work through a subject. The connection drops somewhere in the middle. Maybe they close the app. Maybe the battery dies. Maybe they just put the phone away.
What they expect is that the work is waiting for them later. What the naive implementation gives them is a handful of decks and no indication that eleven topics were silently dropped. The checkbox is ticked, so the app appears to believe the material exists. It does not.
This is the worst shape a bug can take: the interface says the work is done, and the data says it never happened.
Write it down before you try
The fix starts with ordering. The topic is written to a pending list before any attempt to generate it, not after a failure.
That single reversal is what makes the feature crash-safe. If the app dies mid-request, the record already exists — the next launch picks it up. If instead you recorded a failure only when a request failed, a process that never returned has nothing recorded, and the topic is lost with no trace that it was ever requested.
Each pending entry holds the subject, the topic, when it was asked for, and how many times it has been tried.
Both things, or neither
Generating a topic produces two artifacts: a flashcard deck and a quiz. They are separate requests and either can fail on its own.
A pending entry is cleared only when both exist. Not when the request returns, not when one of them succeeds — when the store can be asked and both say yes.
That check makes the whole thing idempotent. Re-ticking a topic that is already half-done does not re-generate the half that exists, and a retry after a partial success only does the missing piece. The student can tick and untick as much as they like; the AI is not called twice for the same deck.
Unticking a topic removes it from the pending list, so an accidental tap does not leave a ghost request running.
Backing off, and not wasting attempts
Retries run on a timer with exponential backoff, starting at a minute and growing to a maximum of fifteen. A success resets the delay, so the next topic is not punished for the previous one's failures.
There is one refinement that matters more than the backoff itself. Before burning an attempt, the app checks whether there is a network at all. A phone sitting in a pocket with no signal would otherwise tick through its retries every few minutes, exhaust them, and give up — all while offline. The work would then be dropped for the exact reason it should have been preserved.
So an offline tick is not an attempt. It waits.
Waking up at the right moment
The queue is wired to run when the app launches, so anything left over from last time resumes immediately.
It also runs when an AI provider key is finally configured. That case is easy to forget: a student who installed the app and ticked topics before setting anything up has a pending list full of work that could never have run. The moment they add a key, it starts.
Sharing the room with everything else
This retry engine is one of several background jobs that can wake at the same moment, so it does not get to call the AI whenever it likes — it goes through the same queue as the rest of the background work, one topic at a time. That ordering problem is its own story: five AI workers walking into a rate limit at the same time.
What matters here is the division of labour. The queue decides when a topic is generated. The pending list decides whether it still needs to be. Neither can do the other's job, and mixing them is how you end up with work that is either lost or done twice.
The general shape
Anywhere the app promises a student that something will be ready, and the work depends on a network that comes and goes, the same three rules apply:
- Record the intent before attempting it, so a crash loses nothing.
- Clear it only when the finished thing can be observed, not when the call returns.
- Never spend a retry on a state where retrying could not possibly work.
A checkbox in a study app is a promise. It should be one the app can keep.
Keep reading
Related reading
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.
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.
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.