An event is a message that says something changed: a producer emits it, a consumer reacts to it, and a broker sits between the two so neither side has to know about the other. Most engineers already understand that mechanism before they come asking about event-driven architecture, because what they actually need is narrower than the concept. It's usually a webhook that fans out to three services, or a background job that shouldn't block the response, and neither of those needs a Kafka cluster. We've watched teams stand one up anyway, because the word "event" showed up somewhere in the requirements doc.
We run event-driven workloads at the scale where the pattern genuinely earns its keep. GitBook resolves 40,000 daily cache invalidations across 30,000 documentation sites, each one in under 300ms, and the network-bound routing inside AI Gateway runs on the same underlying pattern. Running event-driven workloads at that scale settles the question for us: the real decision was never whether to adopt event-driven architecture as a philosophy, but which of a handful of primitives fits the workload sitting in front of you. Get the primitive right and the complexity earns its place. Get it wrong, and you end up paying broker latency for something a direct function call would have handled for free.
Copy link to headingKey takeaways
Event-driven architecture describes a spectrum of patterns connecting producers and consumers through a broker.
Serverless functions are a natural fit for the consumer side, but wall-clock billing punishes the idle time event consumers spend waiting on I/O.
Vercel ships four async primitives:
after(), Queues, Workflows, and Cron, each built for a distinct workload shape rather than for event-driven architecture as a whole.Picking the wrong primitive costs real debugging hours, since traces go opaque across async boundaries and delivery guarantees are weaker than they sound.
Webhook-triggered ISR revalidation has the strongest production evidence on our platform. At GitBook's scale, invalidations resolve in under 300ms.
Copy link to headingWhat is event-driven architecture
The mechanism itself is simple: a producer emits an event, a consumer reacts to it, and a broker sits in between so the two never have to coordinate directly. Everything else in event-driven architecture is a variation on how that broker behaves.
Pub/sub is the simplest of those variations, since the broker doesn't track which messages have been received and consumers are left to manage their own delivery and load. Fan-out is closer to what most people picture when they hear "event-driven": one published message reaches every subscribed consumer, and each reacts independently, on its own schedule, without waiting on the others. Event sourcing pushes further still, treating every state change as an event and deriving the system's current state by replaying that log rather than storing it directly.
What all three patterns promise underneath is loose coupling, the ability for services to deploy and scale independently of one another. We've watched that benefit hold up often enough to trust it, but it doesn't show up early. It shows up once a team is operating at a scale where independent deployment is actually worth what it costs, and below that line, teams end up paying for decoupling they don't need yet.
The question this piece actually answers is which of a handful of primitives fits the shape of the specific workload in front of you, one primitive at a time.
Copy link to headingServerless functions are natural event consumers until billing gets in the way
A consumer that spins up when an event arrives and disappears once its work is done is exactly what serverless was built for. It only needs to exist for one event, with no long-running process sitting idle in between. On paper, that makes serverless functions the obvious home for event consumers.
Traditional serverless billing gets in the way of that fit. AWS Lambda bills for wall-clock time, so a function making a blocking call gets billed while it waits for a response to come back. Event consumers spend most of their lives in exactly that kind of wait, sitting on database writes and upstream APIs, and paying CPU rates for that idle time turns the architecturally correct pattern into the economically wrong one.
Active CPU pricing exists as the direct answer to that mismatch. Billing pauses during I/O waits, so teams pay compute rates only while their code is actually running on a vCPU rather than parked mid-request. Our own AI Gateway is the clearest proof of what that does in practice: it spends most of its runtime waiting on network calls, and under Active CPU pricing it's billed at CPU rates for less than 8% of that runtime instead of the full 100%. Event consumers have the same shape, waiting far more than they compute, and the billing model finally reflects that.
A single Fluid compute instance also handles multiple in-flight requests at once, so when one invocation pauses for I/O, another executes in that same instance instead of spinning up cold, removing the cold-start tax on bursty event traffic. One caveat: the concurrency benefit only shows up once request concurrency exceeds one, so a single-threaded batch job processing one job at a time won't see it. Know which side of that line a workload sits on before counting on the win.
Copy link to headingFour primitives, four workload shapes
Vercel ships four async primitives, and each one is built for a distinct workload shape rather than for event-driven architecture as a general practice. Function execution across all four runs on Fluid compute where applicable, Queues and Workflows carry their own usage-based charges on top of that, and the relevant compute metrics and configuration surface in the Vercel dashboard. Identify the shape of the workload first, and the primitive follows on its own.
Unlike AWS SQS, Queues ships with no built-in dead-letter queue. Failed messages get retried automatically until expiry, honoring the configured retry delay for the first 32 attempts before forcing exponential backoff after that. Poison messages, the ones that fail on every single attempt, don't get caught by any of this. Detecting and routing them is an application-level job, and it's one worth building before the first poison message shows up rather than after.
We've watched teams skip the Queues SDK and hit a visibility-timeout mismatch as a result. The SDK defaults the visibility timeout to 300 seconds and auto-extends the lease for as long as the handler runs. The raw API defaults to 60 seconds and doesn't auto-extend anything, so a slow handler calling the raw API directly can have its own message redelivered while it's still mid-processing, less a Queues limitation than a reason to reach for the SDK instead of the raw API by default.
Copy link to headingWhat picking the wrong primitive costs
Event-driven architecture gets reached for by systems that are synchronous by nature far more often than the reverse. When the architecture doesn't match the problem, it's pure over-engineering, and we've watched that mismatch cost teams more than the complexity it was meant to avoid. Coupling problems get caught in code review. The cost of picking the wrong primitive shows up in production, weeks after the decision, and it shows up as a debugging problem before it shows up as anything else.
A synchronous call gives you a stack trace you can read top to bottom. An event, once it crosses a broker, gives you a correlation ID scattered across however many services touched it, assuming anyone remembered to propagate that ID in the first place. Without deliberate context propagation, work spread across services produces isolated trace fragments instead of one execution graph you can follow start to finish.
We built Workflows to close that gap for durable multi-step work, specifically, with run-level traces and logs, metrics on every run, and pause, replay, and time-travel debugging built in. For Queues, structured logs with requestId correlation and an OpenTelemetry integration via @vercel/otel are how teams bridge the same tracing gap. The gap narrows in both cases, but it never fully closes.
Users notice the seams even when engineers don't. Every async boundary in a system is a place where the interface can show stale data right after someone clicks something that should have changed it.
Delivery guarantees compound the problem. Every message queue that makes any promise at all promises at-least-once delivery, and any queue claiming not exactly-once isn't being straight about how it actually works. At-least-once delivery means idempotent handlers aren't optional. Queues supports idempotency keys through the Vqs-Idempotency-Key header, but the handler logic that actually makes retries safe is still work the team has to write.
One account making the rounds describes exactly what happens when that work gets skipped: retry logic on a failed event ended up charged five times over, billing a single customer repeatedly for the same order. For a 15-person startup, a mistake like that doesn't just cost the refund. It costs three months of runway once support time and lost trust get added in, and the team behind that account eventually moved back to a modular monolith just to start shipping again.
The pattern underneath all of these failure modes is the same one: they show up hardest when a workload only ever has one consumer. If a single service is the only thing that will ever process a given event, that setup is indirect function calling with extra latency and a broker sitting in the middle for no reason. A direct call, or after() when the side effect belongs after the response, does the identical job with none of the overhead.
Copy link to headingWhere event-driven architecture earns its complexity on Vercel
Once a workload genuinely fans out to multiple independent consumers, the pattern pays for itself, and the best-documented case on our platform is webhook-triggered ISR revalidation. A CMS event like content publishing triggers targeted revalidation for the pages actually affected, avoiding a full rebuild the site doesn't need.
GitBook runs this at a scale that makes the case on its own: 30,000 documentation sites on a single Vercel deployment, 120 million monthly page views served from the edge, and 40,000 cache invalidations a day, each resolved in under 300ms, event-driven architecture doing precisely what it promises at a scale where the alternative would mean rebuilding thousands of sites every time someone hits publish.
Fan-out earns its place in smaller cases too, not just at GitBook's scale. A new_user_created event fired from a stateful user database can trigger independent serverless functions for email notification, onboarding initialization, analytics tracking, and trial setup, with each one scaling on its own. Four independent consumers reacting to one event is the shape where Queues beats a chain of direct calls, since none of those four functions need to know the others exist or wait on each other to finish.
Webhook ingestion carries its own trust problem before any of this works. Provider webhooks arrive from the outside, and Vercel Connect handles verification, re-signing the forwarded request with an identity the app can check so it never has to hold a signing secret of its own. For teams handling ingestion directly, the starting point is a Route Handler at app/api/webhooks/route.ts with Stripe signature verification using stripe.webhooks.constructEvent.
Copy link to headingFAQ
Copy link to headingDoes Vercel Queues have a dead-letter queue?
No. Failed messages get retried automatically, honoring the configured retry delay for the first 32 attempts before forcing exponential backoff until the message expires. Catching and routing poison messages is an application-level responsibility, not something Queues handles on its own.
Copy link to headingWhen should I use after() instead of Vercel Queues?
Reach for after() when there's a single deferred side effect per request, like logging or analytics, that doesn't need a retry or a fan-out to multiple consumers. Move to Queues once more than one consumer group needs to process the same event independently, or once the work needs retry semantics and durability across redeploys.
Copy link to headingIs Vercel Queues production-ready?
It's listed as Beta in Vercel's pricing documentation. The Queues pricing limits, a 100 MB message size cap, a 7-day TTL, and unlimited consumer group concurrency, are documented and working today, along with idempotency key support. The two things worth weighing before committing are the missing dead-letter queue and the approximate-only ordering, both of which matter more for workloads that are ordering-sensitive or prone to poison messages.
Copy link to headingMatching the primitive to the problem eliminates more complexity than any architecture pattern
Choose the primitive that actually matches the workload in front of the team rather than the one that matches the ambition behind it. A single post-response side effect belongs in after(), durable fan-out with retries belongs in Queues, and multi-step orchestration that has to survive a redeploy belongs in Workflows. Scheduled work is a job for Cron.
Look across everything in this piece and the pattern is consistent: every case above reduces to identifying the shape of the workload honestly, and the primitive falls out from there. If one service consumes the event and nothing else ever will, a direct call or after() beats a broker on latency, cost, and debuggability all at once. If the workload genuinely fans out or needs durability across deploys, the async stack is worth its operational overhead, and GitBook's 40,000 sub-300ms daily invalidations are proof that the overhead pays for itself at the right scale.