Performance Engineering
April 2026 · 7 min read

How We Scaled a Next.js App to Handle 10k Daily Active Users

A deep-dive into the architectural decisions — edge caching, ISR, and database connection pooling — that allowed us to serve 10,000 daily users on a $20/month infrastructure budget.

Next.jsPerformanceVercelISRArchitecture

When we started building StreetBite, our geolocation platform for connecting food lovers with local street vendors, we assumed traffic would grow slowly. We were wrong. Within three weeks of launch, we were seeing over 10,000 daily active users — and our initial architecture was showing serious cracks.

The Problem: Slow Pages and a Spiraling Cloud Bill

Our first version used server-side rendering (SSR) on every page. Every request hit the Next.js server, which hit our Spring Boot API, which hit PostgreSQL. On a slow day, that chain averaged 800ms. On a busy Friday evening — when street food foot traffic peaks — it spiked to over 3 seconds.

We were burning compute unnecessarily and delivering a poor experience. We needed a fundamentally different approach.

Solution 1: Incremental Static Regeneration (ISR) at the Edge

The first thing we did was rethink our rendering strategy. Vendor listings — the most visited pages — don't change every second. A vendor's location, hours, and menu are updated at most a few times a day.

We switched these pages to ISR with a revalidation window of 300 seconds (5 minutes). This meant Vercel's edge network served pre-rendered HTML to the vast majority of users, with no backend involvement. The average page load time dropped from 800ms to under 80ms — a 10× improvement overnight.

typescript
// app/vendor/[id]/page.tsx

export async function generateStaticParams() { const vendors = await getTopVendors(); // Pre-render top 500 return vendors.map(v => ({ id: v.id })); } ```

Solution 2: Database Connection Pooling with PgBouncer

Our Spring Boot API was opening a new PostgreSQL connection for every request under load. With 10,000 DAU, this quickly exhausted Postgres's connection limit (default: 100).

We deployed PgBouncer in transaction pooling mode in front of our RDS instance. This reduced the number of active database connections from ~200 under load to a stable ~15, while still serving all concurrent requests. Our database CPU dropped by 60%.

Solution 3: Aggressive API Response Caching with Redis

Search results — "street food near me" queries — are expensive. They involve a PostGIS spatial query, a JOIN across two tables, and a distance calculation for every result row.

We introduced a Redis layer with a 60-second TTL for geolocation queries, keyed by a grid-snapped coordinate (to a ~500m precision). Within those 60 seconds, identical or near-identical searches return cached results in under 5ms.

The cache hit rate stabilised at 73%, meaning nearly three-quarters of all search requests never touched the database.

The Result

After three weeks of iteration, our architecture could handle 10,000 DAU comfortably on a $20/month Vercel Pro plan and a $15/month RDS micro instance. P99 latency on vendor pages sits at 94ms. Our database connection count never exceeds 20.

The lesson: over-engineering upfront is expensive. But so is ignoring rendering strategy and database connection management. Profile first, then fix the right things.