|----------|-------------------|-----------------------|---------------------|------------------|
| Client Polling (3s) | 420 | 180 | 11.4% | 310 |
| Direct API Writes | 195 | 950 | 7.8% | 485 |
| Optimized Queue + SSE | 48 | 140 | <0.1% | 215 |
Key Findings:
- Atomic Redis increments reduce DB write pressure by ~85% while maintaining sub-50ms response times.
- SSE eliminates polling overhead, reducing bandwidth consumption by ~72%.
- The sweet spot lies in separating the hot path (vote ingestion + real-time broadcast) from the cold path (persistent storage + analytics).
Core Solution
The architecture leverages Next.js App Router, TypeScript strict mode, and a hybrid persistence strategy. Vote ingestion hits an atomic Redis counter, while a background worker batches writes to PostgreSQL. Real-time updates are pushed via SSE to connected clients.
1. TypeScript Payload & Type Safety
// types/poll.ts
export interface VotePayload {
pollId: string;
optionId: string;
userId: string;
timestamp: number;
idempotencyKey: string;
}
export interface PollState {
pollId: string;
options: Record<string, number>;
totalVotes: number;
lastUpdated: number;
}
2. Next.js API Route (Vote Ingestion)
// app/api/vote/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { redis } from '@/lib/redis';
import { VotePayload } from '@/types/poll';
export async function POST(req: NextRequest) {
const payload: VotePayload = await req.json();
// Idempotency check
const alreadyVoted = await redis.get(`vote:${payload.userId}:${payload.pollId}`);
if (alreadyVoted) {
return NextResponse.json({ error: 'Duplicate vote' }, { status: 409 });
}
// Atomic increment
const pipeline = redis.pipeline();
pipeline.hincrby(`poll:${payload.pollId}:options`, payload.optionId, 1);
pipeline.incr(`poll:${payload.pollId}:total`);
pipeline.set(`vote:${payload.userId}:${payload.pollId}`, '1', { ex: 86400 });
await pipeline.exec();
// Trigger SSE broadcast (implementation omitted for brevity)
return NextResponse.json({ status: 'accepted' }, { status: 202 });
}
3. Architecture Decisions
- Runtime:
nodejs for API routes requiring Redis/BullMQ; edge reserved for static poll metadata.
- State Sync: SSE endpoint streams Redis pub/sub events to clients, avoiding WebSocket overhead.
- Persistence: BullMQ worker consumes a 5-second batch window, executing a single
INSERT ... ON CONFLICT UPDATE per poll.
Pitfall Guide
- Ignoring Idempotency Keys: Network retries or double-clicks cause duplicate votes. Always enforce
userId + pollId uniqueness with short-lived Redis keys before processing.
- Race Conditions in Direct DB Updates:
UPDATE polls SET votes = votes + 1 without SELECT FOR UPDATE or atomic operations leads to lost increments under concurrency. Use Redis HINCRBY or PostgreSQL UPDATE ... RETURNING.
- Misconfigured Next.js API Timeouts: Serverless functions default to 10s timeouts. Long-running SSE connections or unoptimized DB queries will trigger
504 Gateway Timeout. Implement proper res.end() and streaming headers.
- TypeScript Type Looseness: Using
any for dynamic poll options or form state breaks compile-time validation. Enforce strict interfaces and use zod for runtime validation of API payloads.
- Over-fetching Client State: Re-rendering the entire poll component on every vote update causes layout thrashing. Use React
useTransition or isolate vote counters in memoized sub-components.
- Edge Runtime Incompatibility: Importing Node-specific modules (e.g.,
fs, net, bullmq) in Edge API routes causes deployment failures. Explicitly set runtime: 'nodejs' where backend dependencies are required.
- Missing Rate Limiting: Unthrottled vote endpoints are vulnerable to DDoS and bot spam. Implement token bucket or sliding window rate limiting at the API gateway or edge middleware level.
Deliverables
- π Architecture Blueprint: Visual flow diagram detailing Client β Next.js API β Redis (Hot Path) β BullMQ β PostgreSQL (Cold Path) β SSE Broadcast pipeline.
- β
Pre-Deployment Checklist:
- βοΈ Configuration Templates:
next.config.js (runtime routing, headers for SSE)
tsconfig.json (strict compiler options, path aliases)
redis.conf & bullmq-worker.ts (batch window, retry policies, connection pooling)