Event Processors: Subscribing vs Tracking
Learn how Axon actually delivers stored events to projections and event handlers, and why choosing between a subscribing and a tracking event processor changes a system's latency, scalability, and crash-recovery behavior.
Learning objectives
- Explain how a subscribing event processor differs from a tracking event processor in timing and threading
- Identify the risk of running slow logic inside a subscribing event handler
- Describe what a tracking token is and why it enables replay and restart recovery
- Configure an event processor's mode explicitly in Spring configuration
- Choose the right processor type for a given projection's requirements
◆ Imagine this
A phone call happens on the caller's schedule, not yours — you have to be available at the exact moment it rings, or you miss it completely. A mailbox works differently: mail arrives and simply sits there, safely, until you're ready to open it. Go on vacation for a week and the mailbox doesn't lose anything; it just accumulates, waiting for you to catch up at your own pace.
Every projection or event handler in an event-sourced system faces the same choice. Something has to actually deliver a stored event to the piece of code that reacts to it — a read-model updater, a notification sender, a fraud check. In Axon, that delivery job belongs to an event processor, and it comes in exactly two flavors: a subscribing processor, which behaves like the phone call, and a tracking processor, which behaves like the mailbox.
Picture the customer read model for a bank's account-opening flow. When AccountOpenedEvent is stored, something has to notice it and update a queryable row so the customer's dashboard shows the new account a moment later. Whether that update happens synchronously, in lockstep with the command that opened the account, or asynchronously, on its own schedule, is entirely a function of which processor type is doing the delivering. That distinction is small to configure but large in its consequences, and it's worth understanding precisely before you pick one by default.
A subscribing event processor receives events synchronously, on the very same thread that published them, the instant they're applied inside a command handler. There's no queue, no separate worker, no delay — the event handler's code runs as a direct continuation of the command that produced the event, before that command is considered fully handled.
This immediacy sounds appealing at first: it guarantees that by the time the original command "returns," every subscribing handler listening for that event has already finished running. For a system that genuinely needs a read model updated before it responds to the caller, that guarantee is useful. But it comes at a real structural cost — the projection's own performance now sits directly on the critical path of the command that triggered it.
▲ The real risk
If a subscribing @EventHandler does something slow — a heavy database write, a call to an external service, a large in-memory recomputation — that slowness directly delays the response the original command gives back to its caller. The processor runs inline, before the command handling can be considered finished, so there's no way for the command to "move on" while the handler catches up later. A loan-approval command that triggers a subscribing handler which happens to call a slow credit-scoring API will feel exactly as slow as that API, every single time.
Because a subscribing processor runs entirely in memory, on the thread that's already there, it also has no way to recover a lost position. If the application crashes moments after an event was applied but before every subscribing handler finished, there's no durable record of "how far" that handler got — it simply starts fresh on the next event it happens to see, with no memory of what it missed while the process was down.
A tracking event processor runs on its own, separate thread and keeps a durable record — called a token — of exactly which event it processed last for a given segment of the event stream. This single design choice decouples the projection's speed entirely from the command's speed: the original command finishes as soon as its event is durably stored, full stop, and the tracking processor catches up afterward, on its own schedule, at whatever pace it can actually sustain.
That token is more than a performance detail — it's what makes several other capabilities possible at all. Because the processor always knows precisely which event it last handled, it can resume from that exact position after a restart instead of starting over, and it can be told to rewind and replay a stream from any earlier point, including the very beginning, entirely on command.
| Property | Subscribing | Tracking |
|---|---|---|
| Timing | Synchronous — runs inline with the command | Asynchronous — runs on its own thread, its own pace |
| Can replay history? | No | Yes — the token remembers exactly where it left off |
| Can run in parallel, across threads? | No | Yes — work can be split across segments for higher throughput |
| Recovers its position after a restart? | No — starts fresh | Yes — the stored token says exactly where to resume |
◆ Under the hood — what the token actually buys you
A tracking processor's token is the single piece of state that makes replay and crash recovery possible at all. Because the processor durably records "I've processed up through event N for this segment," it can always answer the question "where was I?" — whether that question comes from a restarted process picking back up, or from an operator deliberately asking it to reprocess history from scratch to rebuild a read model that got corrupted or needs a new column. A subscribing processor, with no durable position of its own, can answer neither question.
Axon defaults every event processor to tracking mode, and for most projections that default is the right call — the decoupling, replay support, and restart recovery it provides are valuable for nearly any read model you'll build. You can still be explicit about the mode per processing group, which is good practice once a service has more than one projection with different needs.
| Use | When |
|---|---|
| Tracking (the default, and the sensible starting point) | Almost always — the decoupling, replay support, and restart recovery are genuinely valuable for nearly every real projection, including the customer, account, card, and loan read models in a typical bank system. |
| Subscribing | Only when a projection update must be guaranteed complete before the original command's response returns — a narrow, specific requirement, not a general-purpose choice. |
Configuring the mode is a one-line change per processing group in application.yml, naming the processing group you want to affect:
axon: eventhandling: processors: account-projection: mode: tracking # or "subscribing"
A processing group is simply the name Axon uses to bucket a set of event handler methods together under one processor. By default, that name comes from the class's package, but it's common to set it explicitly with @ProcessingGroup("account-projection") on the component so the configuration key above lines up predictably, independent of how the code happens to be packaged.
💻 Code example
@ProcessingGroup("account-projection") @Component public class AccountProjection { private final AccountViewRepository repository; public AccountProjection(AccountViewRepository repository) { this.repository = repository; } @EventHandler public void on(AccountOpenedEvent event) { AccountView view = new AccountView(); view.setAccountId(event.getAccountId()); view.setCustomerId(event.getCustomerId()); view.setBalance(event.getInitialBalance()); repository.save(view); } }
▲ Common mistake — reaching for subscribing "for consistency"
It's tempting to assume a subscribing processor gives you a stronger consistency guarantee, since the handler finishes before the command returns. In practice this trades a real, measurable cost — slower commands, no crash recovery, no replay — for a guarantee that's rarely actually required. Most callers don't need to see an updated read model in the same response; they need it updated soon, reliably, and that's exactly what tracking provides.
▲ Edge case — tracking handlers must tolerate redelivery
Because a tracking processor's progress is only as fresh as its last committed token, a crash between "handler ran" and "token saved" means the same event can be delivered again after recovery. A projection's event handlers should therefore be idempotent — safe to apply twice — rather than assuming each event arrives exactly once. Something like repository.save(view) on a full overwrite of the row is naturally idempotent; an UPDATE balance = balance + amount style increment is not, and needs a different approach, such as checking a processed-event marker first.
▲ Edge case — segments and ordering
A tracking processor can be split into multiple segments to process events in parallel for higher throughput. Axon guarantees that all events for one aggregate — say, one specific account — always land in the same segment and are processed in order relative to each other. It does not guarantee any ordering between events from different aggregates. A handler that assumes "account A's event always arrives before account B's" when it shouldn't is a subtle, easy mistake to make once parallel segments are in play.
In a typical bank system built on Axon, the overwhelming majority of projections use tracking processors by default: the account balance read model, the card list shown on a customer's dashboard, the loan status view — all of these can tolerate being a few milliseconds or even seconds behind the write side, and all of them benefit from surviving a restart without losing their place.
Subscribing processors show up in narrower situations — for example, a synchronous uniqueness check that must complete before a command handler can report success, or a lightweight in-memory cache update that's cheap enough that inlining it costs nothing measurable. Some teams also use a subscribing processor temporarily while debugging, specifically because its synchronous nature makes cause and effect easier to observe step by step, then switch back to tracking before deploying.
Tracking processors are also what makes horizontal scaling of read-model updates practical: splitting a high-volume processor into multiple segments, each running on a different thread or even a different service instance, lets a bank's account-projection keep up with a busy day's transaction volume without falling behind, something a single-threaded subscribing processor could never do.
- Q: What's the core timing difference between subscribing and tracking event processors? A: Subscribing runs synchronously, inline with the command, on the same thread. Tracking runs asynchronously, on its own thread and its own pace.
- Q: What does a tracking event processor's token actually enable? A: Resuming from an exact known position after a restart, and replaying event history on command — neither is possible with a subscribing processor.
- Q: Why is it risky to put slow logic inside a subscribing event handler? A: Because the handler runs inline before the command is considered finished, its slowness directly delays the response to whoever sent the command.
- Q: What does Axon default new event processors to, and why is that usually the right choice? A: Tracking mode — because the decoupling, replay support, and restart recovery it provides are valuable for nearly every real-world projection.
- Q: Why must a tracking processor's event handlers be written to be idempotent? A: Because a crash between running the handler and saving its token can cause the same event to be redelivered after recovery.
Want a visual for this concept?
Generate a diagram tailored to “Event Processors: Subscribing vs Tracking” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →