In early 2025, we were running a SaaS platform for a logistics company processing roughly 12,000 API calls per hour. The product worked. The code passed every review. The test coverage was respectable. Then the client doubled their operational territory — and the whole thing fell over in three days.

This isn't a post about fixing bugs. It's about the architectural assumptions we made when building for "current scale" that made the system fundamentally brittle at the next order of magnitude. Here's what we found, what we changed, and the specific numbers that justified each decision.

42ms Avg response before
1,840ms Avg response at 10× load
38ms After redesign, same load

The Monolith Problem Is Not What You Think

Everyone assumes the fix is "go microservices". That's wrong. The problem wasn't that we had a monolith — it was that our monolith had no internal boundaries. Every domain bled into every other domain through shared database tables and synchronous function calls. Billing logic queried the same connection pool as tenant provisioning. Authentication middleware ran a fresh DB query on every single request.

When load tripled, the connection pool saturated. When the pool saturated, every request waited. When every request waited, timeouts cascaded. The monolith wasn't the problem — the lack of isolation was.

The root failure mode isn't architectural style — it's the absence of resource isolation between domains. A well-structured monolith outperforms a poorly designed microservices system every time.

— Rahul Nair, Lead Engineer

What We Actually Changed

1. Read Replica Routing

Our first win was the simplest. We added a read replica and routed all non-mutating queries to it. This sounds obvious in retrospect. Roughly 73% of our queries were reads — analytics dashboards, reporting endpoints, list views. Offloading them to the replica immediately halved the write-primary's CPU utilisation and cut median response times by 60% before we touched a single line of business logic.

TypeScript
// Before: every query hits the primary
const result = await db.query('SELECT * FROM orders WHERE tenant_id = $1', [id]);

// After: reads route to replica automatically
const result = await db.replica().query('SELECT * FROM orders WHERE tenant_id = $1', [id]);

// Writes still go to primary
await db.primary().query('INSERT INTO orders ...', [orderData]);

2. Event-Driven Domain Isolation

The deeper fix was introducing Redis Streams as an event bus between internal domains. Instead of the billing module calling the tenant module synchronously, billing emits an event. The tenant module consumes it asynchronously. The domains are now decoupled at the I/O boundary. A spike in billing processing no longer blocks tenant provisioning, and vice versa.

We didn't rewrite the entire system. We identified the five highest-latency cross-domain call chains, extracted them to events, and re-deployed. The whole exercise took two engineers eleven working days.

3. Aggressive Connection Pool Tuning

Our default pg pool was set to 10 connections per process instance. We were running four instances. At 10× load we were spawning retry threads faster than connections were being released. We profiled every query, killed three that were genuinely unbounded, added a 5-second statement timeout to catch regressions, and tuned the pool to 25 connections with pgBouncer in front at the infrastructure layer. Connection wait time dropped from 800ms average to 4ms.

The Numbers That Justified Each Decision

Engineers often skip this part. Don't. Every architectural change has a cost — engineering time, deployment risk, operational complexity. You need numbers to make the case internally and to know which intervention deserves priority.

P95 Latency (ms) — Before vs After Each Change 1800 900 300 0 1840 740 380 62 38 Baseline + Replica + Events + PG Tune Final

What We'd Do Differently From Day One

If we were starting this product again, here's what changes in the first two weeks of engineering:

Closing Thought

Scale failures are rarely about the technology stack. They're almost always about the assumptions baked into the design at a point when scale didn't matter. The fix isn't a rewrite — it's isolation, observability, and the patience to profile before you build.

If you're running a SaaS product and noticing latency climbing as you grow, the patterns here likely apply. We're happy to do a free architecture review — link at the bottom.

RN
Rahul Nair
Lead Engineer · Nepras Technologies
Rahul leads backend engineering at Nepras with a focus on distributed systems, database performance, and SaaS architecture. He has built and scaled platforms across fintech, logistics, and healthcare — and has a particular obsession with query plan analysis at 3am.