Pub/Sub vs API Calls: Should Your Services Publish an Event or Just Call Each Other?
A user signs up, and email, push, and analytics all need to know. Do you call each service yourself, or publish one event and walk away? Here's Pub/Sub vs direct API calls — event-driven async messaging vs synchronous request/response — with a clear rule for which one to reach for.
A user signs up. Now email, push, and analytics all need to know about it. Do you call each of those services yourself, one by one — or do you publish one event and walk away? That question — Pub/Sub or direct API call — is one of the most consequential (and most habit-driven) decisions in a microservice architecture.
The one-line mental model:
- Direct API calls are a phone call: you dial the other service, wait on the line, and hang up once you have your answer.
- Pub/Sub is a radio broadcast: you announce something once, into the air, and anyone tuned in picks it up — you never know (or care) who's listening.
Direct API calls: request in, response out
The signup service calls the email service, waits for the reply, then calls the push service, waits for the reply, then calls analytics, waits for the reply. Simple, synchronous, and honest: you get an immediate, confirmed answer.
POST /email/welcome HTTP/1.1
Host: email-service.internal
Content-Type: application/json
{ "user_id": 42 }
200 OK
{ "status": "sent" }
That immediacy is exactly what you want when the caller can't continue without the answer — "authorize this payment," "fetch this user," "check this password." There's no ambiguity about whether it happened; you know before your next line of code runs.
The cost shows up the moment you fan out to more than one dependency:
- Tight coupling — the signup service has to know the address of email, push, and analytics. Add a fourth consumer tomorrow, and you edit the caller.
- N blocking calls — fan-out to 3 services means 3 (or 3× sequential, or 3 concurrent-but-still-waited) round trips before the request can finish.
- No buffer — if the push service is slow or down, the whole signup request stalls or fails, even though push notifications have nothing to do with whether the signup itself succeeded.
Pub/Sub: publish once, let subscribers do the rest
Pub/Sub inverts the relationship. The signup service publishes one event — UserSignedUp — to a broker (Kafka, RabbitMQ, or a cloud queue like SNS+SQS) and moves on immediately. Email, push, and analytics each subscribe to that event and pick it up on their own schedule.
// published once, to a topic — not to any specific service
{
"event": "UserSignedUp",
"user_id": 42,
"email": "user@example.com",
"occurred_at": "2026-07-03T10:00:00Z"
}
The publisher never knows — or needs to know — who's listening. Add a fourth consumer (say, a fraud-check service) tomorrow, and nothing upstream changes. That gives you:
- Decoupling — the signup service has zero knowledge of email/push/analytics.
- 1 → N fan-out for free — one publish, however many subscribers care.
- A buffer — if the push consumer is slow or briefly down, it just reads its backlog off the queue when it recovers. The signup request already finished.
The catch is real, not cosmetic:
- You now run and operate a broker. Kafka, RabbitMQ, or SNS+SQS is infrastructure you own, monitor, and keep healthy.
- No immediate answer. The signup request returns before email/push/analytics have actually run — it's eventually done, not done-now.
- At-least-once delivery. Most brokers can redeliver a message, so every consumer needs to be idempotent (processing the same
UserSignedUpevent twice must be safe). - Harder tracing. A synchronous call chain shows up as one trace. An event fan-out means the "was this handled?" question spans a broker and N independent consumers, each on its own timeline.
Side by side
| Direct API call | Pub/Sub | |
|---|---|---|
| Style | Synchronous request/response | Asynchronous publish/subscribe |
| Coupling | Tight — caller knows every consumer | Loose — publisher knows nothing about subscribers |
| Fan-out | N blocking calls | 1 publish → N subscribers, free |
| Answer | Immediate, confirmed | Eventual — no return value to wait on |
| Slow dependency | Stalls or fails the whole request | Buffered — consumer catches up later |
| Delivery | Exactly this call, this once | At-least-once — consumers must be idempotent |
| Infra cost | None beyond the services themselves | A broker to run (Kafka / RabbitMQ / SNS+SQS) |
| Tracing | One clean call chain | Harder — spans broker + independent consumers |
The rule
Need an immediate answer for a simple 1:1 request? Make the direct API call — "authorize this payment" has no business being eventually consistent.
Need to fan one event out to many services, stay decoupled, and survive a slow or down consumer? Reach for Pub/Sub — it's exactly why notification pipelines (welcome emails, push, analytics, fraud checks) are built on it.
Most real systems use both, in different places: a synchronous API for the request that needs an answer right now, and an event published on the side for everything that can happen a moment later. It's not a religious war between the two — it's recognizing which kind of question you're actually asking.
The same tradeoff shows up in LLM serving
If you're building anything around LLM inference, this exact fork reappears — just with different names.
A synchronous chat completion call is the direct-API pattern: the client sends a prompt, holds the connection open (or streams tokens as they're generated), and gets the response in that same request. It's the right shape when a human is staring at the screen waiting for the answer — that's why chat UIs stream tokens over the open connection instead of making the user poll.
But plenty of LLM workloads don't have a human waiting in real time — bulk document summarization, running an eval suite, generating embeddings for a whole corpus, batch-classifying support tickets. For those, providers like OpenAI expose a Batch API: you submit a file of requests, get a job id back immediately, and the results land in object storage (or hit a webhook) whenever the job completes — often hours later, at a steep discount. That's the Pub/Sub shape wearing an inference costume: publish the job, walk away, let a worker pool pick it up, and get notified (not polled) when it's done.
The same tradeoffs apply almost one-for-one: batch inference is cheaper and survives a slow GPU node without blocking anything, but you lose the immediate answer and now need idempotent handling if a job gets retried. Multi-step agents hit this constantly too — a tool call that must finish before the next reasoning step is a direct call; a long-running background task (a nightly re-indexing job, a multi-hour research agent) that reports back on completion is Pub/Sub. Same fork, same rule: does the next step need the answer right now, or can it be told later?
Want the 90-second visual version? Watch the reel.