Synchronous Event Chain
Learn how event-triggered synchronous calls stack latency and amplify failures across a service chain, and how async event handling breaks the coupling.
TL;DR
- A synchronous event chain occurs when an event triggers a service call, which triggers another event, which triggers another call, all synchronously in the same request thread.
- Each hop in the chain adds its latency to the total response time. Each service in the chain is a potential failure point that propagates back to the original caller.
- This pattern often hides in event frameworks: "process order event β call inventory service β call email service β call analytics service." What looks like an event pipeline is a synchronous call chain in disguise.
- Break the chain with async: publish events to a queue, let consumers handle them independently without blocking the original caller.
- Latency in a synchronous chain is additive (sum of all hops). Latency in an async system is determined by the critical path alone (the slowest operation the user must wait for).
30-second explanation
A synchronous event chain is a request path that calls one listener after another while the original caller waits. Latency adds across every hop, and a failure in a non-critical listener can fail the user-visible operation. Keep only the work required for the response on the critical path; publish the rest durably for independent consumers.
5-minute explanation
Trace the request from the user to the first service and then follow each event handler. If an event publication waits for the handler, or the handler calls another service before returning, the chain is synchronous regardless of the frameworkβs event terminology. Separate critical work such as reserving inventory from non-critical work such as analytics and email. Put the latter behind a durable queue, give consumers their own retry and dead-letter policies, and keep explicit timeouts and idempotency for any critical downstream call that remains synchronous.
The Problem
Consider this illustrative scenario: it's 11:02 a.m. on launch day. A checkout team deployed a "simple" order event handler last week. When a user places an order, the Order service creates the record, then fires an order.created event. The event sounds async, but every listener processes synchronously in the same request thread.
Here's the actual call chain:
The user waited about 3.4 seconds for an order confirmation in this illustrative scenario. Their request was blocked while Warehouse called Metrics, which called Analytics. Analytics had a slow database query that day. The user's "Place Order" button spun, they clicked again, and a duplicate order was possible.
This failure is easy to miss because the Order service team may not realize that its latency depends on the Analytics database. The chain often becomes visible only after examining a distributed trace.
The latency budget was consumed by non-critical downstream calls (analytics, metrics) that did not need to be in the synchronous critical path of order creation. Removing the 3,200ms Analytics call leaves about 180ms of listed work (Order, Inventory, Warehouse, and Metrics), or roughly 190ms with request and network overhead. Removing all non-critical calls leaves 130ms of listed critical work.
Here's the breakdown of where the time went:
| Service | Latency | Critical for user? | Should be sync? |
|---|---|---|---|
| Order Service | 50ms | Yes (creates order) | Yes |
| Inventory Service | 80ms | Yes (confirms stock) | Yes |
| Warehouse Service | 30ms | No (internal logistics) | No |
| Metrics Service | 20ms | No (observability) | No |
| Analytics Service | 3,200ms | No (dashboard) | No |
| Total (sync chain) | 3,380ms | ||
| Total (critical only) | 130ms |
In this illustrative table, the full chain is about 3.38 seconds, compared with 130ms of listed critical work. That gap is the cost of keeping non-critical operations synchronous.
Why It Happens
Teams build synchronous event chains because each decision makes sense in isolation.
"Events are async by nature." Not necessarily. Many event frameworks (Spring ApplicationEvent, Node.js EventEmitter, .NET MediatR) dispatch events synchronously by default. The word "event" can make a call seem non-blocking, but the handler may run in the calling thread and block the original request until it returns.
"We just need one more listener." The chain grows incrementally. First it's Order + Inventory (reasonable, 2 services). Then someone adds email notification. Then analytics. Then fraud scoring. No single addition looks dangerous, but by month six, your checkout latency depends on five downstream services.
"All our services are fast." They may be under normal conditions, until one is not. Latency in a synchronous chain is additive: if five services each take 50ms, the service time is 250ms before network and framework overhead. When one service hits a slow query or a connection pool timeout, the whole chain stalls; an illustrative 3-second Analytics delay can turn a fast checkout into a multi-second timeout.
"We need the result before we can respond." Sometimes true (inventory check), but teams apply this reasoning to every operation. Ask: "Does the user need this result to see their confirmation page?" If the answer is no, it doesn't belong in the synchronous path.
Real-world examples
This anti-pattern appears wherever "event-driven" meets "in-process dispatch":
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.