Back to blog
Architecture
AdvancedForPlatform EngineersBackend Engineers
5 min

Pub/Sub or Eventarc? Event-Driven GCP Without the Spaghetti

When to reach for Pub/Sub, when for Eventarc, and how to keep an event-driven GCP system clean: idempotency, dead-letter queues, and the trap of fan-out spaghetti.

event-drivenpub-subeventarcgcpmessagingarchitecturecloud-events
Contents

The first event-driven system feels like magic: a file lands in a bucket, a function fires, a record appears. The tenth feels like a crime scene. Nobody can say what triggers what, a duplicate just charged a customer twice, and one bad message has been retrying since Tuesday.

Event-driven on GCP is not hard because the tools are weak. It goes sideways because two services that look similar — Pub/Sub and Eventarc — get used interchangeably, and the boring parts (idempotency, dead-letter) get skipped until production teaches them.

When the file lands, the function fires, and nobody can explain why.

This is the field guide: which one to reach for, how to keep handlers safe, and how to stop the fan-out from turning into spaghetti.

Routing is one half of a solid GCP platform; locking it down is the other. For the security baseline your clusters run on, see Secure-by-Default GKE: A Reference Architecture for 2026.

Pub/Sub vs Eventarc: One Transports, One Routes

They overlap, so people pick by habit. The clean rule:

  • Pub/Sub is the messaging backbone. You publish events you own to a topic; consumers subscribe. You control schema, ordering, retention. Use it for your own domain events — order.placed, payment.settled.
  • Eventarc is a router on top. It takes events Google already emits — Cloud Storage uploads, Audit Logs, 90+ sources — and delivers them to a target as one CloudEvents contract. Use it to react to the platform without writing glue.

You rarely choose one over the other; Eventarc rides on Pub/Sub anyway. The question is “who produces this event?” If it’s you, Pub/Sub. If it’s Google, Eventarc.

Event-driven GCP at a glance: Google emits to Eventarc, you publish to Pub/Sub, fan out to idempotent subscribers, dead-letter the poison.

Terminal window
# Your own event → Pub/Sub
gcloud pubsub topics create order-placed
gcloud pubsub subscriptions create fulfilment --topic=order-placed
# A GCP event (file uploaded) → Eventarc, no glue code
gcloud eventarc triggers create on-upload \
--destination-run-service=thumbnailer \
--event-filters="type=google.cloud.storage.object.v1.finalized" \
--event-filters="bucket=my-uploads"

At-Least-Once Means: It Will Arrive Twice

Both services deliver at-least-once. That is not an edge case — design for it. A retried delivery, a slow ack, a redeploy mid-flight, and the same event lands again. If “twice” means a double charge, that’s a bug you wrote, not bad luck.

Make every consumer idempotent: dedupe on a stable event ID, and a re-run becomes a no-op.

def handle(event):
eid = event["id"] # stable, provided by Pub/Sub/Eventarc
if seen.add_if_absent(eid): # atomic: returns False if already there
process(event) # do the real work exactly once
# duplicate → silently ack, no side effect

The store can be Redis with a TTL, a unique column, or Firestore. The rule never changes: the second copy must do nothing.

Dead-Letter: One Poison Message Shouldn’t Stop Everything

A single unparseable payload retries forever and blocks the line behind it. A dead-letter topic parks it after N attempts so real traffic keeps moving.

Terminal window
gcloud pubsub subscriptions update fulfilment \
--dead-letter-topic=fulfilment-dead \
--max-delivery-attempts=5

Then alert on the dead-letter topic. A message there is a question to answer, not noise to ignore — but the rest of the flow never stalls.

Push or Pull: How Subscribers Get Their Events

Before topology, get the delivery model right. A Pub/Sub subscription is either push or pull, and beginners pick wrong then fight the platform:

  • Push — Pub/Sub POSTs each message to an HTTPS endpoint (Cloud Run, a function). Zero polling, scales to zero, ideal for serverless. The endpoint must answer fast and return 2xx to ack.
  • Pull — your worker asks for batches and acks them. Best for high-throughput backends, long jobs, or fine control over concurrency.

Rule of thumb: serverless target → push; always-on worker → pull. Eventarc to Cloud Run is push under the hood — you do not choose, and that is fine.

Ordering: Usually You Don’t Want It

Pub/Sub can keep order within an ordering key, but ordering throttles throughput and most flows don’t need it. Design consumers so arrival order doesn’t matter: handling order.placed then order.cancelled should be correct either way. Only enable ordering keys when state truly depends on sequence (a per-account ledger), and key on the entity, never globally.

Terminal window
gcloud pubsub topics publish order-events --message="..." --ordering-key="acct-42"

Publish a Fact, Not a Blob

A topic is a contract. Events should be versioned, self-describing, and small — IDs, not whole records. Carry a type, a stable id, and a v so consumers can evolve without coordination:

{ "id": "evt_9f3", "type": "order.placed", "v": 1, "orderId": "A42", "ts": "2026-06-29T10:00:00Z" }

Fat payloads couple producer and consumer; the ID lets each subscriber fetch exactly what it needs. Add fields freely, never break or rename one — bump v instead.

When NOT to Go Event-Driven

Events are not free, and the worst spaghetti is async where sync would do. Reach for a plain call or Cloud Tasks when:

  • You need a result now — a synchronous request, not a broadcast.
  • One known producer, one known consumer — that is a queue; use Cloud Tasks with retries and rate limits, not a topic.
  • A two-step flow — a direct call beats a topic nobody else subscribes to.

Use Pub/Sub when many consumers care about the same fact and producers must not know who listens. If only one does, you bought spaghetti for nothing.

The Spaghetti: Stop Chaining Triggers Blind

The real mess isn’t the tools — it’s topology. A fires B, B fires C, C quietly fires A again, and nobody drew the map. Three rules keep it flat:

  1. Topic = fact, not command. Publish order.placed, not send.email. Producers state what happened; consumers decide what to do. New behaviour = new subscriber, zero producer changes.
  2. Fan out, don’t chain. Many subscribers on one topic beat a 6-hop relay. Parallel paths are debuggable; chains hide cycles.
  3. One source of truth for routing. Keep triggers in Terraform, not click-ops. If the graph only lives in someone’s head, it’s already spaghetti.

The Field Rule

Pub/Sub when you produce the event, Eventarc when Google does. Assume every event arrives twice and make handlers idempotent. Dead-letter the poison so one bad message can’t stop the line. Publish facts, fan out instead of chaining, and keep routing in code. Get those right and event-driven stays the magic it felt like on day one — instead of the crime scene it becomes by article ten.

Frequently asked questions

What is the difference between Pub/Sub and Eventarc?

Pub/Sub is the raw messaging backbone — you publish and subscribe to topics and own the routing. Eventarc sits on top of it and routes ready-made events from Google services (Cloud Storage, Audit Logs, 90+ sources) to a target with one CloudEvents contract. Reach for Pub/Sub when you produce your own events; reach for Eventarc when you react to events Google already emits.

Do I still need Pub/Sub if I use Eventarc?

Almost always yes — Eventarc uses Pub/Sub under the hood for most paths and gives you a managed topic anyway. The choice is not either/or: Eventarc is the routing layer, Pub/Sub is the transport. You pick Eventarc to avoid wiring source-to-target glue, and you still get a Pub/Sub topic you can attach extra subscribers to.

Why do I need idempotent consumers?

Both Pub/Sub and Eventarc deliver at-least-once, so the same event can arrive twice. If a duplicate charges a card twice or sends two emails, you have a bug, not bad luck. Make handlers idempotent — dedupe on a stable event ID and treat a re-run as a no-op — and redelivery becomes harmless instead of dangerous.

What is a dead-letter queue and when do I need one?

A dead-letter topic catches messages that fail after N delivery attempts so a single poison event cannot block the whole subscription forever. You need one the moment a stuck message can stall real traffic. Without it, one unparseable payload retries endlessly; with it, the bad event parks aside and the rest of the flow keeps moving.

Should I use push or pull subscriptions?

Push when your target is serverless — Pub/Sub POSTs each message to a Cloud Run or function endpoint, so there is no polling and it scales to zero. Pull when you run an always-on worker that needs high throughput or fine concurrency control. Eventarc to Cloud Run is push under the hood, so you usually do not choose at all.

From the community

Discussion on the Fediverse

Replies from Mastodon and Bluesky — straight from the open web, no tracking.

Loading replies …

ENDE