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.

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.

# Your own event → Pub/Subgcloud pubsub topics create order-placedgcloud pubsub subscriptions create fulfilment --topic=order-placed
# A GCP event (file uploaded) → Eventarc, no glue codegcloud 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 effectThe 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.
gcloud pubsub subscriptions update fulfilment \ --dead-letter-topic=fulfilment-dead \ --max-delivery-attempts=5Then 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.
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:
- Topic = fact, not command. Publish
order.placed, notsend.email. Producers state what happened; consumers decide what to do. New behaviour = new subscriber, zero producer changes. - Fan out, don’t chain. Many subscribers on one topic beat a 6-hop relay. Parallel paths are debuggable; chains hide cycles.
- 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.
