skip to content

What strategies are commonly used to decide when to take a new snapshot of an event-sourced aggregate — for example triggering on event count versus elapsed time — and what trade-offs does each have?

level: middleimportance: must knowfreq 65%

answer

  1. count trigger = every N events
  2. time trigger = scheduled/backstop
  3. combine with OR for both bounds
  4. reactive/opportunistic snapshot after slow load
  5. tune N by watching load latency

basics

~20 s

You can snapshot every N events (like every 500 changes), on a timer (like nightly), or when loading gets slow. Event-count triggers keep replay short and predictable; time-based triggers are simpler but can let busy aggregates drift far behind.

solid answer

~40 s

The two dominant triggers are event-count-based ('snapshot every N events since the last snapshot,' e.g. every 100 or 500) and time-based ('snapshot every X minutes/hours' or on a batch job schedule). Event-count triggers bound the worst-case replay tail directly and adapt naturally to bursty aggregates — a hot aggregate gets snapshotted more often, a cold one rarely. Time-based triggers are simpler to schedule and reason about operationally (e.g., a nightly batch job) but can let a high-write aggregate accumulate thousands of unreplayed events between runs, while wasting snapshot writes on aggregates that had zero activity. Many production systems combine both: snapshot on N events OR every X time, whichever comes first, and some also snapshot opportunistically right after a load that had to replay an unusually long tail.

go deeper

for a junior

Should know that some rule decides when snapshots happen, e.g. 'every so many changes' or 'on a schedule.'

for a middle

Should compare count-based vs time-based triggers and name at least one trade-off for each.

for a senior

Should propose combining triggers and discuss tuning N against measured load latency.

for a principal

Should reason about the async-worker-lag failure mode, synchronous-vs-asynchronous snapshot writes' latency/coupling cost, and per-aggregate-type tuning at scale.

## What the decision actually turns on The decision of when to take a snapshot is fundamentally about controlling the worst-case cost of loading an aggregate, and the two axes you can trigger on are how many events have accumulated since the last snapshot, and how much time has passed. Each has a different failure profile, and most serious implementations combine them rather than pick one exclusively. ## Triggering on event count Event-count-based triggering says: after every N events appended to a stream since the last snapshot (a common choice is somewhere between 50 and 1,000 depending on how expensive each event's apply logic is), write a new snapshot. The appeal is that it directly bounds the thing you actually care about — the number of events that must be replayed on the next load — regardless of how much wall-clock time those events took to arrive. - A **hot aggregate** that receives 10,000 events in an hour gets snapshotted roughly every few minutes. - A **cold aggregate** that receives one event a month might go a year between snapshots and simply never need one, because its full replay is cheap anyway. This adaptivity is the main strength: snapshot frequency naturally tracks write activity, so you spend snapshot I/O where it actually saves replay time, and you don't burn storage snapshotting aggregates that barely change. The weakness is operational: it requires either checking the event count on every append (a counter read-and-compare on the hot write path, which adds a small amount of latency and coupling) or an asynchronous consumer that tails the stream and counts, which introduces the standard 'this consumer fell behind' failure mode. ## Triggering on elapsed time Time-based triggering instead runs on a schedule independent of the event stream's activity — a nightly cron job that snapshots every aggregate touched that day, or a background worker that snapshots any aggregate whose snapshot is more than 24 hours old. The strength is operational simplicity: it's a well-understood batch job, easy to monitor, easy to reason about ('every aggregate has a snapshot no older than a day'), and it doesn't touch the write path or add load-time coupling. The weakness is that it doesn't track write volume at all: an aggregate that receives 50,000 events in the twelve hours right after the last nightly run will have a 50,000-event tail to replay on any load that happens before the next run — the worst-case replay cost is unbounded by any guarantee the scheduler gives you. It can also waste effort snapshotting aggregates that received zero new events since the last snapshot, redoing identical work for no benefit unless the job explicitly skips unchanged aggregates. ## The two triggers side by side | Trigger | Fires on | Main strength | Main weakness | |---|---|---|---| | **Event-count-based** | N events since the last snapshot | adaptivity — it directly bounds the number of events replayed on the next load, regardless of wall-clock time | operational — a counter read-and-compare on the hot write path, or the 'this consumer fell behind' failure mode | | **Time-based** | a schedule independent of the event stream's activity | operational simplicity — a well-understood batch job, easy to monitor and easy to reason about | worst-case replay cost unbounded under bursty write patterns; can also waste effort on aggregates that received zero new events | Because each trigger fails in the case the other one handles well, many real systems combine them with an OR condition: snapshot when N events have accumulated since the last snapshot, or when X time has passed, whichever comes first — this caps replay-tail length under bursty write patterns (the count trigger fires) while still guaranteeing a maximum snapshot age under low-write patterns (the time trigger fires as a backstop). A further refinement some systems use is **reactive/opportunistic snapshotting**: if a load operation ends up replaying an unusually long tail (say, past some threshold), the loading code writes a fresh snapshot right then, on the theory that if it was expensive for this reader it will likely be expensive for the next one too. This trades a slightly more expensive read for cheaper subsequent reads and is self-correcting even if the proactive trigger's tuning was wrong. ## What each direction costs The concrete cost on both sides is worth naming precisely. More frequent snapshotting means: - more storage consumed (snapshots for the same aggregate accumulate unless old ones are pruned); - more write I/O and potential lock contention if the snapshot write touches the same storage as the event append; - more surface area for the staleness/versioning bugs described elsewhere (more snapshot versions in flight means more chances for a race between concurrent readers/writers). Less frequent snapshotting means cheaper writes and storage but a growing tail that eventually degrades read latency and, at the extreme, can make an aggregate effectively unloadable in a reasonable time budget if nobody notices the metric. ## How it lands in practice A concrete real-world instance: Akka Persistence (a JVM actor persistence framework used for event-sourced actors) exposes a simple count-based policy out of the box — you configure 'snapshot every N persisted events' per persistent actor — and leaves time-based or opportunistic strategies to application code layered on top, which is exactly the pattern of picking a simple default (count) and composing additional triggers where the workload demands it. In practice, teams tune N empirically: start around a few hundred events, watch p99 aggregate-load latency, and adjust down for hot aggregates or up for cheap-to-replay ones.

  • Why might a purely event-count trigger still leave you with an unbounded worst case?
    If checking/writing the snapshot is asynchronous and the async worker falls behind (e.g., it's overloaded or crashes), an aggregate can keep accumulating events well past the configured threshold before a snapshot actually lands, so the nominal 'every N events' guarantee is only as strong as the worker's liveness.
  • What's a downside of writing the snapshot synchronously, inline with the command that crosses the threshold?
    It adds snapshot serialization and storage-write latency directly to that command's response time, and couples the write path's availability to the snapshot store's availability — if the snapshot store is briefly down, you don't want that to fail or slow down normal writes.
  • How would you choose the count threshold N in practice?
    Measure per-event apply cost and target read-latency SLA, then pick N so that replaying up to N events stays comfortably under budget; validate empirically with p99 load latency and adjust per-aggregate-type if some aggregates have much heavier apply logic than others.

Like deciding when to save your progress in a video game: save every 10 rooms cleared (count-based) versus autosave every 5 minutes (time-based) — a speedrunner clearing rooms fast benefits from count-based saves, while a slow explorer benefits from the time-based backstop.

saying these in an interview costs you the question

  • says snapshot timing doesn't matter for performance
  • assumes time-based snapshotting bounds replay length
  • doesn't recognize count-based needs a fallback for async lag
  • picks N arbitrarily with no measurement of apply cost

context