Building Event-Driven Architecture with Inngest
Transforming API responses from seconds to milliseconds with durable execution

Transforming API responses from seconds to milliseconds with durable execution

"Abstract illustration of light-speed data transfer through digital prism with orange, blue, and purple color spectrum representing high-performance network architecture"
Illustration by Perplexity Labs
4 posts in this series
Feel free to send us a message with your thoughts, or learn more about us!
A multi-part series on building production-ready developer platforms: implementing CSP, rate limiting, INP optimization, analytics, and comprehensive security features.
How I used Next.js App Router, Tailwind v4, and shadcn/ui to build a production-ready developer portfolio with modern architecture patterns.
A short, honest changelog: what shipped in the @dcyfr/ai v3.x line, what changed for people building on it, and what comes next — catching the blog up with work the package already produced.
Part 3 of 4
Series Background: This is Part 3 of the Portfolio series. . Following the initial build and security hardening, here we explore how event-driven architecture transforms user experience and system reliability using Inngest for background job processing.
Your API routes are lying to your users. They return 200 OK while work is still happening. The contact form says “Message sent!” but the email hasn’t been delivered yet.
Event-driven architecture fixes this by separating acknowledgment from processing. Users get instant feedback. Work happens reliably in the background. Here’s how makes this practical for any project.
When I first built this portfolio’s contact form, the flow was straightforward but slow:
// The old way: synchronous processing
export async function POST(request: NextRequest) {
const { name, email, message } = await request.json();
// User waits 1-2 seconds for this...
await resend.emails.send({
from: FROM_EMAIL,
to: AUTHOR_EMAIL,
subject: `Contact form: ${name}`,
text: message,
});
// Only then do they see success
return NextResponse.json({ success: true });
}This approach has real problems:
The fix is decoupling the response from the work, not making the email faster.
separates two concerns:
Before (Synchronous):
User → API Route → Email Service → Response
└─────── 1-2 seconds ──────┘
After (Event-Driven):
User → API Route → Queue Event → Response (< 100ms)
↓
Background Function → Email Service
└─── Retries if needed ───┘Event-driven architecture transforms API reliability and user experience. This post demonstrates real production results: 10x faster response times, 99.9% delivery reliability, and automated security monitoring, all without managing queue infrastructure.
Modern event-driven systems add steps: discrete, named operations with checkpoints. If step 3 fails, steps 1 and 2 don’t re-run. This transforms reliability thinking.
Further Reading: For comprehensive overviews of event-driven architecture patterns, see Martin Fowler’s Event-Driven Architecture and AWS’s What is Event-Driven Architecture?
Implement event-driven architecture in any Next.js project with Inngest. Use step functions for durable execution, automatic retries with exponential backoff, and a local dev UI for debugging. Zero infrastructure required.
Here’s the actual production code from this portfolio:
// src/app/api/contact/route.ts
import { inngest } from '@/inngest/client';
export async function POST(request: NextRequest) {
const { name, email, message } = await request.json();
// Validate and sanitize inputs...
// Queue the event (returns immediately)
await inngest.send({
name
The API route now completes in under 100ms. The user sees instant feedback.
// src/inngest/contact-functions.ts
import { inngest } from './client';
import { Resend } from 'resend';
import { track } from '@vercel/analytics/server';
export const contactFormSubmitted = inngest.createFunction(
{
id: 'contact-form-submitted',
retries: 3, // Automatic retries with exponential backoff
Each step.run() creates a checkpoint. If Step 2 fails:
This is : your function survives failures and resumes from the last successful step.
Event-driven architecture isn’t just for user actions. Scheduled tasks benefit too.
The homepage shows a GitHub contribution heatmap. Instead of fetching on every page load (slow, rate-limited), I pre-populate the cache hourly:
// src/inngest/github-functions.ts
export const refreshGitHubData = inngest.createFunction(
{
id: 'refresh-github-data',
retries: 1, // Fail fast on hourly jobs
},
{ cron: '0 * * * *' }, // Every hour at minute 0
async ({ step }) => {
await step.run('fetch-github-contributions', async ()
Benefits:
Here’s what’s actually running right now:
| Function | Trigger | Purpose |
|---|---|---|
contact-form-submitted | Event | Send notification + confirmation emails |
refresh-github-data | Hourly cron | Pre-populate contribution heatmap cache |
track-post-view | Event | Update view counts, track daily analytics, detect milestones |
calculate-trending | Hourly cron | Compute trending posts from recent views |
|
After discovering a critical React vulnerability (React2Shell) in December 2025 (with a 13-hour detection gap), I added automated security monitoring.
export const securityAdvisoryMonitor = inngest.createFunction(
{
id: 'security-advisory-monitor',
retries: 3,
},
{ cron: '0 0,8,16 * * *' }, // 3x daily (00:00, 08:00, 16:00 UTC)
async ({ step }) => {
// Step 1: Fetch advisories from GHSA
const advisories = await step.run('fetch-ghsa-advisories'
The blog tracks views and automatically detects milestones:
export const trackPostView = inngest.createFunction(
{ id: 'track-post-view' },
{ event: 'blog/post.viewed' },
async ({ event, step }) => {
const { postId, slug, title } = event.data;
await step.
Inngest provides a local dev server with a powerful UI:
npx inngest-cli@latest devThis gives you:
Functions are async functions; test them like any other code:
describe('contactFormSubmitted', () => {
it('sends notification and confirmation emails', async () => {
const mockEvent = {
data: {
name: 'Test User',
email: 'test@example.com',
message: 'Hello!',
submittedAt: new Date().toISOString(),
Inngest integrates with Vercel in a few steps:
1. Install the Vercel integration in your Inngest dashboard
2. Export functions from a single endpoint:
// src/app/api/inngest/route.ts
import { serve } from 'inngest/next';
import { inngest } from '@/inngest/client';
import { contactFormSubmitted } from '@/inngest/contact-functions';
import { refreshGitHubData } from '@/inngest/github-functions';
import { trackPostView, calculateTrending } from '@/inngest/blog-functions';
import { securityAdvisoryMonitor
3. Deploy. Inngest discovers your functions automatically
Environment variables (INNGEST_EVENT_KEY, INNGEST_SIGNING_KEY) are set automatically by the Vercel integration.
After migrating to event-driven architecture (measured in this portfolio’s production environment):
| Metric | Before | After |
|---|---|---|
| Contact form response time | 1–2s (observed) | Under 100ms (observed) |
| Email delivery reliability | ~95% (observed) | 99.9% (with 3 retries, based on Inngest’s exponential backoff) |
| GitHub data freshness | On-demand | Pre-cached hourly |
| Failed job visibility | None | Full dashboard |
| Security advisory detection | Manual | Automated (3x daily) |
Note: These metrics reflect this specific implementation. Your results may vary based on network conditions, third-party API performance, and deployment region.
Event-driven architecture doesn’t require enterprise scale. With tools like Inngest, any Next.js project can benefit from reliable background processing, instant API responses, and better observability.
Background jobs are a security surface. This post covers automated CVE monitoring (3x daily GHSA checks), input validation in async pipelines, and how a 13-hour detection gap led to building automated security advisory monitoring.
Event-driven architecture isn’t always the answer:
The rule of thumb: if users don’t need to wait for the result, don’t make them wait. But if they do need to see the result immediately, don’t hide it behind a queue.
Background jobs are a security surface. The security monitor exists because CVE detection gaps matter. If processing sensitive data, apply the same rigor you would to API routes: validate inputs, sanitize outputs, monitor for anomalies.
| Inngest | Serverless, local dev UI, automatic retries | Newer ecosystem | Docs |
What sold me on Inngest:
| Hourly cron |
| Pre-compute activity feed for instant page loads |
security-advisory-monitor | 3x daily | Check GHSA (GitHub Security Advisory database) for CVEs affecting dependencies |
daily-analytics-summary | Daily cron | Generate previous day’s blog analytics |
sync-vercel-analytics | Daily cron | Sync Vercel analytics to Redis for dashboards |
When a post hits 1,000 views, I get notified. The trending calculation runs hourly, scoring posts by recent activity to surface what readers are finding valuable.