Performance·July 8, 2026

What actually breaks when a web app meets real traffic

A demo works fine on localhost, with one user and a warm cache. Learn the three common assumptions that quietly break once real traffic shows up at scale.

A web app in local development, with one browser tab and a warm dev server, tells you almost nothing about how it behaves under real traffic. Most of the issues that surface after launch aren't bugs in the code, the code works. They're assumptions that held at zero concurrent users and stop holding at fifty.

Key takeaways

  • Most post-launch issues aren't bugs, they're assumptions that held at zero concurrent users and break at fifty.
  • Serverless functions can exhaust your database's connection cap within seconds during a traffic spike.
  • Caching is opt-in in modern frameworks, not a default that protects your app on its own.
  • Marking a whole page as client-rendered for one small widget can cost real, measurable load time on mobile.

Why local development doesn't tell you much about real traffic

A web app in local development, with one browser tab and a warm dev server, tells you almost nothing about how it behaves under real traffic. Most of the issues that surface after launch aren't bugs in the code, the code works. They're assumptions that held at zero concurrent users and stop holding at fifty.

Database connection exhaustion

One of the most common issues is database connection exhaustion from serverless functions. Every invocation of a serverless API route can open its own database connection, and Postgres has a hard cap on how many it accepts, often around 100 by default, depending on version and configuration.

A traffic spike that triggers dozens of concurrent function invocations can exhaust that cap within seconds, and every request past the limit fails outright. The fix is a connection pooler sitting between the app and the database, not simply provisioning a bigger database instance, the same fine-in-development, slow-in-production shape as a missing database index.

Caching that was assumed, not configured

A second common cause is caching that was assumed rather than actually configured. Modern frameworks ship real, powerful caching primitives, for example static rendering, timed revalidation, and per-route caching rules, but they're opt-in decisions, not defaults that quietly protect an app on their own.

A page that should be static but ends up rendering dynamically re-hits the database on every single request instead of once per revalidation window. That distinction is invisible with one user locally and becomes the entire story once thousands of people load the same page in an hour.

Client-side bundle size

A third issue is client-side bundle size, specifically from interactive code placed higher in the component tree than it needs to be. Marking a whole page as client-rendered because one small widget on it needs interactivity drags every dependency that widget uses into the bundle sent to every visitor, even the ones who never touch that widget.

It costs nothing on a fast connection during development, and costs real, measurable load time on an ordinary mobile connection.

Catching these before launch

None of these show up in a demo. They show up in the first real week of usage, which is exactly why we recommend load-testing actual endpoints, checking connection pool behavior under concurrency, and auditing where interactivity boundaries sit before launch, not after.

Common mistakes

  • Assuming caching is on by default. Modern frameworks ship real caching primitives, but they're opt-in, not automatic.
  • Opening one database connection per serverless invocation without a pooler. A traffic spike can exhaust the connection cap within seconds.
  • Marking a whole page client-rendered for one small widget. This drags every dependency that widget uses into the bundle sent to every visitor.
  • Testing only with one local user. That tells you almost nothing about how the app behaves under real concurrent traffic.

FAQ

Why does a web app that works in development break under real traffic?

Development testing happens with one user and zero concurrency. Most production issues are assumptions that only break once real concurrent traffic shows up.

What's the fix for database connection exhaustion?

A connection pooler sitting between the app and the database, not simply a bigger database instance.

The bottom line

The code usually works. What breaks under real traffic is the assumptions underneath it: connection limits, caching that was never actually configured, and bundle size from interactivity placed too high in the component tree. Catch these with load testing before launch, not after.

Preparing for real traffic before launch? Talk to our team.

Tell us what you're building.

Describe the problem in plain language. An engineer reads every message and replies within one business day.

Write to Us
Reply
Within one business day
First call
Free, no commitment
Confidentiality
NDA on request, before you share anything