Spaced Repetition for Technical Learning: SM-2 Algorithm

Jun 9, 2026
15 min read

AI Insights

Powered by GPT-4o-mini

Verified Context: spaced-repetition-for-technical-learning-sm-2-algorithm
Quick Answer

How the SM-2 spaced repetition algorithm is implemented, how review schedules are computed from reading history, and why SRS beats bookmarks for long-term retention.

Quick Summary

Discover how the SM-2 algorithm optimizes technical learning through spaced repetition, improving retention and review efficiency.

Spaced Repetition for Technical Learning

Bookmarking a post means "I'll remember to come back to this." A spaced repetition system means "the system will remind me at the optimal time, based on how well I retained the information."

This blog implements the SM-2 algorithm — the same one used by Anki and SuperMemo — adapted for technical reading rather than flashcard review.


The SM-2 Algorithm

SM-2 schedules reviews based on four parameters per card:

python
class SrsCard(Base):
    __tablename__ = "srs_cards"

    id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
    user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"))
    post_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("posts.id"))
    ease_factor: Mapped[float] = mapped_column(default=2.5)
    interval: Mapped[int] = mapped_column(default=0)  # days
    review_count: Mapped[int] = mapped_column(default=0)
    next_review: Mapped[datetime] = mapped_column(default=func.now())
    last_review: Mapped[datetime | None]
    created_at: Mapped[datetime] = mapped_column(default=func.now())
  • ease_factor — how easy the material was (starts at 2.5, ranges from 1.3 to 5.0)
  • interval — days until the next review (grows with each successful review)
  • review_count — number of times reviewed
  • next_review — the date when the user should review this post again

The SM-2 Review Logic

When a user marks a post as reviewed, the card is updated:

python
SMALL_INTERVAL = 1     # 1 day
MEDIUM_INTERVAL = 6    # 6 days
EASE_MIN = 1.3
EASE_MAX = 5.0

def sm2_review(card: SrsCard, quality: int):
    """quality: 0 (forgot) to 5 (perfect recall)"""
    if quality < 3:
        card.review_count = 0
        card.interval = SMALL_INTERVAL
    else:
        if card.review_count == 0:
            card.interval = SMALL_INTERVAL
        elif card.review_count == 1:
            card.interval = MEDIUM_INTERVAL
        else:
            card.interval = round(card.interval * card.ease_factor)

        card.review_count += 1

    ease_delta = 0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02)
    card.ease_factor = max(EASE_MIN, min(EASE_MAX, card.ease_factor + ease_delta))
    card.next_review = date.today() + timedelta(days=card.interval)
    card.last_review = datetime.utcnow()

The quality parameter maps to a simplified UI:

User ActionQualityEffect
Forgot (couldn't recall the topic)0-1Reset to 1 day interval, ease drops
Hard (vaguely remembered)2-3Interval stays short, ease drops slightly
Good (remembered most)3-4Interval grows normally
Easy (perfect recall)4-5Interval grows faster, ease increases

Automatic Card Creation from Reading Progress

Cards are created automatically when a user completes a post:

python
async def on_post_completed(user_id: str, post_id: str, db):
    existing = await db.execute(
        select(SrsCard).where(
            SrsCard.user_id == user_id,
            SrsCard.post_id == post_id,
        )
    )

    if existing.scalar_one_or_none():
        return  # Card already exists

    card = SrsCard(
        user_id=user_id,
        post_id=post_id,
        next_review=datetime.utcnow() + timedelta(days=1),
    )
    db.add(card)

The card's first review is scheduled for 1 day after completion — the "minimum useful interval" before which the content is still fresh in memory.


The Review Queue

The frontend fetches the user's due reviews:

python
@router.get("/srs/reviews")
async def get_due_reviews(user: User = Depends(get_current_user)):
    cards = await db.execute(
        select(SrsCard, Post.title, Post.slug).join(
            Post, SrsCard.post_id == Post.id
        ).where(
            SrsCard.user_id == user.id,
            SrsCard.next_review <= func.now(),
        ).order_by(SrsCard.next_review.asc())
    )

    return [
        {
            "id": str(card.id),
            "post_title": title,
            "post_slug": slug,
            "ease_factor": card.ease_factor,
            "interval": card.interval,
            "review_count": card.review_count,
        }
        for card, title, slug in cards.scalars()
    ]

The frontend renders a review card:

tsx
function SrsReviewCard({ review, onSubmit }: Props) {
    const [quality, setQuality] = useState<number | null>(null);

    return (
        <div className="card p-6">
            <p className="text-[10px] font-bold uppercase tracking-widest text-amber-500 mb-2">
                Review Due
            </p>
            <h3 className="font-display font-bold text-lg mb-4">
                {review.post_title}
            </h3>
            <p className="text-sm text-white/60 mb-6">
                Previously reviewed {review.review_count} times.
                Current interval: {review.interval} days.
            </p>

            {quality === null ? (
                <div className="flex gap-2">
                    {[
                        { value: 1, label: "Forgot" },
                        { value: 3, label: "Hard" },
                        { value: 4, label: "Good" },
                        { value: 5, label: "Easy" },
                    ].map((btn) => (
                        <button
                            key={btn.value}
                            onClick={() => setQuality(btn.value)}
                            className="px-4 py-2 rounded-xl text-sm font-bold transition-all
                                       border border-white/[0.08] hover:border-amber-500/30"
                        >
                            {btn.label}
                        </button>
                    ))}
                </div>
            ) : (
                <p className="text-sm text-emerald-400">
                    Review recorded. Next review scheduled.
                </p>
            )}
        </div>
    );
}

The Brain Page

The SRS system is surfaced through the "Brain" page at /milestones/brain. It shows:

  • Due reviews — posts that need review today
  • Upcoming reviews — posts scheduled for the next 7 days
  • Mastered topics — posts with >5 successful reviews and ease factor >3.0
  • Streak — consecutive days with at least one review completed
tsx
function BrainClient({ stats }: { stats: BrainStats }) {
    return (
        <div className="space-y-8">
            <DueReviews cards={stats.due} />
            <UpcomingReviews cards={stats.upcoming} />
            <MasteredTopics cards={stats.mastered} />
            <ReviewHeatmap history={stats.history} />
        </div>
    );
}

The heatmap shows review activity over the past 90 days, similar to GitHub's contribution graph. Each cell represents a day, colored by the number of reviews completed.


Why SRS Beats Bookmarks

A bookmark says "I'll come back to this." An SRS card says "the system will bring this back to you at the optimal time." The difference is:

  • Bookmarks decay — average browser bookmark list has 70% unread items. Users forget they exist.
  • SRS schedules adapt — difficult material comes back sooner, easy material comes back later. The spacing optimizes for the forgetting curve.

The forgetting curve says: within 24 hours of reading, you forget ~60% of the content. A review at 24 hours resets the curve to ~90% retention. Each subsequent review at the right interval compounds retention toward near-permanent memory.

Without SRS, you re-read a post when you happen to remember it exists. With SRS, the post reappears when your retention is about to drop below 80%. That's the difference between passive browsing and active learning.


What's Next

The next post covers the premium paywall — how Razorpay integration works, premium gates, early access walls, subscription management, and the webhook handling that makes it all work without a third-party CMS.


Built with FastAPI, PostgreSQL, and the SM-2 algorithm — no third-party flashcard service.

Frequently Asked Questions

What is the SM-2 algorithm used for in this blog?
The SM-2 algorithm is used to schedule reviews based on how well the user retained the information, adapting it for technical reading rather than flashcard review.
What parameters does the SM-2 algorithm use for scheduling reviews?
The SM-2 algorithm uses four parameters: ease factor, interval, review count, and next review date.
How does the 'Brain' page help users in the SRS system?
The 'Brain' page shows due reviews, upcoming reviews, mastered topics, and a streak of consecutive review days, helping users track their progress.
Why is an SRS system considered better than using bookmarks?
An SRS system is considered better because it adapts to the user's retention, bringing back difficult material sooner and easy material later, optimizing for the forgetting curve.
What does the heatmap on the 'Brain' page represent?
The heatmap shows review activity over the past 90 days, with each cell representing a day and colored by the number of reviews completed.

Related Work

See how this thinking shows up in shipped systems.