Back to blog
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-driven
Part ofArchitecture →
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.

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.

Was this article helpful?