Skip to content
← Back to Case Studies
FULL STACK DEVELOPMENT30x capacity, 40% lower infra cost

Scaling a SaaS Platform from 500 to 15,000 Concurrent Users

How we re-architected a Next.js + Node.js platform to handle 30x traffic growth by splitting reads/writes, adding caching, and optimizing the database layer.

Next.jsTypeScriptNode.jsPostgreSQLRedisAWS AuroraECS FargatePrisma

Representative project based on real engagements. Client details anonymized.

The situation

A B2B SaaS platform built with Next.js and Node.js was hitting performance walls. The app worked fine with 500 concurrent users, but during peak hours (9-11 AM), response times spiked to 800ms+ and the database CPU hit 90%. The team was about to launch a marketing campaign that would 10x their user base.

The root cause: a single PostgreSQL instance handled both reads and writes. The most expensive queries were joins across 5-6 tables for the main dashboard view. There was no caching layer. Every page load hit the database directly.

What we changed

Database layer (week 1-2):

  • Migrated from a single RDS PostgreSQL instance to AWS Aurora with 2 read replicas
  • Added PgBouncer for connection pooling (the app was opening a new connection per request)
  • Rewrote the 3 most expensive queries: replaced multi-table joins with materialized views that refresh every 5 minutes
  • Added composite indexes for the most common query patterns

Caching layer (week 2-3):

  • Added Redis (ElastiCache) for session storage and frequently accessed data
  • Dashboard data is cached with a 60-second TTL — users see slightly stale data instead of slow queries
  • API responses for reference data (dropdowns, user lists, config) are cached for 5 minutes

Application layer (week 3-4):

  • Split the monolithic Node.js API into 3 containerized services: auth, core API, and reporting
  • Each service runs on ECS Fargate with auto-scaling based on CPU/memory
  • The reporting service (heaviest queries) scales independently from the auth service

Frontend (week 4):

  • Added Next.js ISR (Incremental Static Regeneration) for pages that change infrequently
  • Implemented optimistic UI updates for common actions (no waiting for server response)
  • Added loading skeletons instead of spinners for perceived performance

The numbers

MetricBeforeAfter
Concurrent user capacity50015,000+
Average API response time320ms45ms
P99 response time under load800ms+120ms
Database CPU at peak90%35%
Infrastructure cost$1,200/month$720/month
Uptime99.5%99.9%

What we learned

The biggest performance win was not adding servers — it was fixing the queries. The materialized views alone cut the dashboard load time by 70%. The connection pooling fix (PgBouncer) was a close second — the app was spending more time opening database connections than executing queries.

The cost reduction surprised the team. Aurora with read replicas sounds expensive, but it replaced the single over-provisioned RDS instance. The auto-scaling Fargate services scale down during off-hours, so they pay for capacity only when needed.

The lesson: scale the database first, add servers last. Most performance problems are query problems, not capacity problems.

Have a similar problem?

Book a short conversation with a senior engineer and we will tell you honestly whether we can help.