All articles

PETPET · Event processing

A user clicked. Now let the request finish.

A product view looks like a tiny event. At scale, though, shopping activity needs a reliable path to storage without turning the API into a queue manager. Here is how I split that work up—and what happens when publishing fails.

October 30, 2026 · 5 min read

The event API validates and identifies a shopping event, publishes it to Pub/Sub, and a Cloud Run worker stores it idempotently in MongoDB. Failed publishes are saved to MongoDB for the legacy worker to recover.
Pub/Sub handles the normal handoff; MongoDB gives the API a recovery path if that handoff fails.

A click is easy to collect. Keeping it useful is harder.

PETPET tracks ordinary shopping actions such as viewing a product, submitting a search, or adding something to a cart. Those events can feed analytics and recommendation features later, but the mobile request should not have to perform all of that downstream work before it can respond.

The API still has an important job up front: validate the event, attach the best available user and session identity, and give the event a stable ID. Then it hands the message to Pub/Sub. Once the broker accepts it, a separate worker can do the persistence work independently of the request handler.

A queue changes the shape of the work

Without a broker, each event is another reason for an API instance to write directly into the analytics collection while also serving product and checkout traffic. With Pub/Sub in the middle, the producer and the database writer no longer need to run as one step. The queue can hold messages while the worker catches up, and the worker can be scaled separately from the API.

That does not mean the API forgets the event and returns immediately. It waits for the publish attempt to finish. If the broker accepts the message, the API returns success; if publishing fails or times out, the fallback decides what to do next.

Make a repeated delivery harmless

Message delivery can repeat, so the worker cannot assume that seeing an event once means it will only ever see it once. It writes the canonical event to MongoDB using the event ID as its identity. A unique index and an insert-only upsert make a duplicate delivery resolve to the existing record instead of creating a second one.

That gives the pipeline a practical form of idempotency at the storage boundary. If MongoDB is temporarily unavailable, the worker reports an error so the push delivery can be retried. If the event was already stored, the duplicate is acknowledged without repeating the write.

There is a fallback for the awkward failure case

A publish can fail before the event reaches Pub/Sub. When that happens, the API can persist the same event in MongoDB with a pending marker for the existing event worker. That worker can pick it up and preserve the best-effort follow-up work instead of leaving the event only in an application log.

This fallback is useful because it makes the failure path explicit: Pub/Sub is the normal route, and MongoDB is the durable handoff when that route is unavailable. It also means the system has more than one way to continue processing without pretending that a failed publish succeeded.

Redis is a side effect, not the event ledger

Product views have one small extra use: keeping a short recent-view list for a signed-in user. The worker updates that list in Redis on a best-effort basis, while MongoDB remains the canonical event store. If Redis is unavailable, the event is still recorded; the cache simply misses that convenience update.

I like that boundary. The cache can help a feature feel immediate, but it should not decide whether the underlying user action exists at all.

The result is a pipeline with somewhere to go when it stumbles

Moving event processing behind Pub/Sub was not about making failures disappear. It was about giving each step a clear responsibility: the API validates and publishes, the worker writes the canonical record, and an event ID makes repeated broker deliveries safe at that write boundary. When publishing cannot happen, a MongoDB record keeps the event recoverable.

That is a more useful promise than “it is asynchronous.” The work is separated, but the system still knows where an event came from, whether it was already stored, and what path remains when the normal handoff is unavailable.