Story: build a real-time chat monitor
A support chat that wakes a human for every message will not survive its first launch week. This build puts machinery between the customer and your team: every message lands on a topic, a function classifies it in flight, an agent answers the routine ones, and the rest reach humans with the conversation already summarized. A dead-letter topic catches whatever the machinery could not, so nothing silently disappears.
This story combines five services — Pub/Sub, Functions, Agents, Secrets, and the Gateway — and it leans on two tutorials you can treat as the appendix: event-driven functions for the delivery mechanics and event-driven functions' dead-letter section for the parking lot.
The shape of the thing
Why a topic, and not a direct call
The tempting version of this system is one function calling the next function. The version above puts a Pub/Sub topic between every pair, and here is what that buys:
- Retries stop being your code. A push subscription redelivers until your function acknowledges. If the triage function is mid-deploy when a message arrives, the message waits — customers do not see your deploy.
- The dead-letter topic is the insurance policy. A message that fails every retry is parked, not lost. You read the parked messages on a pull subscription and decide what to do, in daylight, with the bad message in hand. That is the difference between "we had a bug" and "we lost your chat."
- Fan-out is free. Later, when you want an analytics copy of every message, you add a second subscription to the same topic. Each subscription keeps its own reading position, so the new reader never steals from triage. No rework of the intake path.
- The budget is explicit. A topic claims its
max_bytesfrom your project's storage budget the moment it exists — empty topics spend budget. Four small topics are not free. Read topics and subscriptions before you size them; surprise 409s come from this, nowhere else.
The cast
| Piece | What it is | Its one job |
|---|---|---|
intake | Function behind a Gateway endpoint | Accept the customer's message, publish it to chat-events, answer "got it" fast |
chat-events | Pub/Sub topic | Buffer every message, retry delivery, park the unhandleable |
triage | Function on a push subscription | Classify: routine or escalate |
support-agent | Agent | Answer the routine ones, with sessions so follow-ups work |
escalations | Topic | Hand the rest to humans, message plus agent's summary |
chat-events-dead | Topic + pull subscription | Hold what nothing could deliver, until you arrive |
And one quiet actor: the customer's API key lives in a secret, injected into the functions as environment variables. No key in source control, no key in a deploy command.
The two seams to get right
The acknowledgement deadline is your latency budget. A push subscription gives your function 30 seconds to acknowledge before it redelivers. If your triage function itself invokes the agent and the agent's cold start eats 20 of those seconds, you will redeliver — and re-invoke — under exactly the conditions you least want duplicates. The honest fix: triage should acknowledge fast and publish forward — classify, publish to the next topic, return. Let the agent's work happen behind its own topic with its own deadline.
The agent's summary is the handoff. When triage escalates, the human who picks up the escalation message should not have to ask "what did the customer say?" Publish the escalation with the conversation so far in the body. On this platform that is cheap: the agent's session is already the assembled history, so the escalate-branch of triage asks the agent for a one-paragraph summary and publishes {customer, summary, original_message} in one message.
The build order that works
Build it backwards from the failure:
- Create the three topics first —
chat-events,escalations, andchat-events-dead— because topic creation is admin-only and the storage budget check happens there. Get the 409s before there is traffic. - Deploy
triageandsupport-agentnext. They are the consumers; them existing is what makes the subscriptions healthy. - Wire the push subscription from
chat-eventstotriage, with the dead-letter topic attached. Event-driven functions shows both shapes — a managed trigger for the simple case, a hand-built subscription when you need dead-lettering. You need dead-lettering, so build the subscription. - Deploy
intakeand publish it behind a Gateway endpoint with--auth noneif it is truly public, or an API key if it is your own widget. Whatever you choose, the endpoint fails closed: if the protection cannot be applied, nothing serves. - Break it on purpose. Publish a message your triage cannot parse, watch it retry, watch it park in
chat-events-dead, and read it back with the pull subscription. If you only test the happy path, the dead-letter topic is decoration.
What you just used, and where to read more
| You used | The page that documents it |
|---|---|
| Topics, subscriptions, budgets | Topics and subscriptions |
| Push delivery, retries, dead-letter | Event-driven functions |
| The function contract | HTTP and events |
| Agents answering with context | Agents overview, Sessions |
| Public entry | Publish an endpoint |
| The keys between services | Use secrets in workloads |