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"
Photo by Perplexity Labs
3 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.
On January 13, 2026, Node.js released security patches for 8 vulnerabilities (3 HIGH, 4 MEDIUM, 1 LOW) affecting all active release lines. This post breaks down each CVE, explains who is affected, and provides actionable remediation guidance.
Part 3 of 3
Series Background: This is Part 3 of the Portfolio series. Hardening a Developer Portfolio and Shipping a Developer Portfolio. 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 isn’t making the email faster—it’s decoupling the response from the work.
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 ───┘The insight: users don’t care when the email sends—they care that you acknowledged their message.
Modern event-driven systems add one more concept: steps. Instead of one monolithic background function, you break work into discrete, named operations. Each step becomes a checkpoint—if step 3 fails, steps 1 and 2 don’t re-run. This is called , and it transforms how you think about reliability.
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?
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: 'contact/form.submitted',
data: {
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
},
{ event: 'contact/form.submitted' },
async ({ event
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 () => {
const contributions =
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 |
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', async () =>
This runs three times daily, checks for CVEs affecting React/Next.js/RSC packages, verifies against my actual installed versions, and alerts me before I read about it on Twitter.
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.run('process-view', async () => {
Inngest provides a local dev server with a powerful UI:
npx inngest-cli@latest devThis gives you:
Functions are just 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(),
},
};
const result =
Inngest integrates seamlessly with Vercel:
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 } from '@/inngest/security-functions';
import { refreshActivityFeed } from '@/inngest/activity-cache-functions';
export
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.
More importantly: users notice. The contact form feels instant. The contribution heatmap loads immediately. Security issues get flagged before they become problems.
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.
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.
One more thing: background jobs are a security surface too. The security monitor in this portfolio exists because I learned the hard way that CVE detection gaps matter. If you’re processing sensitive data in background functions, apply the same security rigor you would to API routes—validate inputs, sanitize outputs, and monitor for anomalies.
The contact form still says “Message sent!”—but now it’s actually true (or will be, with retries, within seconds).
| Inngest | Serverless, local dev UI, automatic retries | Newer ecosystem | Docs |
What sold me on Inngest:
| Hourly cron |
| Compute trending posts from recent views |
refresh-activity-feed | 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.