
Scaling from 100 to 10,000 Users: What Broke and What We Learned
It was 2 AM, and I was staring at my laptop screen watching our server response times climb from 200ms to 8 seconds. A story of unexpected growth, broken databases, and the lessons learned in the war room.
It was 2 AM, and I was staring at my laptop screen watching our server response times climb from 200ms to 8 seconds. My coffee had gone cold hours ago. This wasn't how I imagined my startup internship would go, but here I was, in the middle of what would become the most valuable learning experience of my career.
The Calm Before the Storm
When I joined the startup as an intern, we had around 100 daily active users. Our application was a simple web platform built with a standard stack - Node.js backend, PostgreSQL database, and a React frontend hosted on a single cloud server. Everything worked smoothly. Response times were snappy, deployments were straightforward, and life was good.
Then we got featured.
When Everything Falls Apart
It started on a Tuesday morning. Our founder had secured a mention in a popular tech newsletter with over 50,000 subscribers. We were excited but didn't think much of it - surely not everyone would sign up, right?
Wrong.
By noon, we had gone from 100 users to over 2,000. By evening, we hit 5,000. And that's when things started breaking.
The first sign: Users started complaining about slow load times on our community feed. What used to take 2 seconds was now taking 30 seconds or timing out completely.
The second sign: Our authentication system started failing. Users couldn't log in, and new signups were getting stuck in an infinite loading state.
The breaking point: Around 8 PM, the entire application went down. Complete outage. Nothing worked.
The All-Night War Room
I called my mentor (who was thankfully understanding about the 9 PM call), and we dove into debugging mode. Here's what we discovered over the next several hours:
Problem #1: Database Drowning in Connections
Our PostgreSQL database was configured with a maximum of 20 connections. Each API request was opening a new database connection and not releasing it properly. With thousands of concurrent users, we quickly exhausted the connection pool.
The quick fix: I increased the max connections to 100 and implemented proper connection pooling using pg-pool. But this was just a band-aid.
// Before (Bad)
app.get('/api/posts', async (req, res) => {
const client = await pool.connect();
const result = await client.query('SELECT * FROM posts');
res.json(result.rows);
// Connection never released!
});
// After (Good)
app.get('/api/posts', async (req, res) => {
const client = await pool.connect();
try {
const result = await client.query('SELECT * FROM posts');
res.json(result.rows);
} finally {
client.release(); // Always release!
}
});
Problem #2: The N+1 Query Nightmare
Our main feed was loading posts and then making separate database queries for each post's author information, comments count, and likes count. With 50 posts on the feed, that's 150+ database queries per page load.
The fix: I spent two hours rewriting our queries to use JOINs and aggregate functions. One query instead of 150.
-- Before: 1 query for posts + N queries for metadata
SELECT * FROM posts LIMIT 50;
-- Then for each post:
SELECT username FROM users WHERE id = ?;
SELECT COUNT(*) FROM comments WHERE post_id = ?;
SELECT COUNT(*) FROM likes WHERE post_id = ?;
-- After: Single optimized query
SELECT
p.*,
u.username,
u.avatar_url,
COUNT(DISTINCT c.id) as comment_count,
COUNT(DISTINCT l.id) as like_count
FROM posts p
LEFT JOIN users u ON p.user_id = u.id
LEFT JOIN comments c ON p.id = c.post_id
LEFT JOIN likes l ON p.id = l.post_id
GROUP BY p.id, u.id
ORDER BY p.created_at DESC
LIMIT 50;
This single change reduced our feed load time from 8 seconds to under 500ms.
Problem #3: Missing Indexes
Around 3 AM, I realized we had almost no database indexes. Every query was doing full table scans. With 100 users, this wasn't noticeable. With 10,000 users and hundreds of thousands of records, it was catastrophic.
The fix: I added indexes on foreign keys and frequently queried columns:
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);
CREATE INDEX idx_comments_post_id ON comments(post_id);
CREATE INDEX idx_likes_post_id ON likes(post_id);
CREATE INDEX idx_users_email ON users(email);
Query performance improved by 10-20x instantly.
Problem #4: CPU Maxed Out
Our single server was running at 100% CPU. The Node.js process was handling everything - API requests, image processing, email sending, you name it.
The fix: This required a bigger architectural change:
- I set up Redis for caching frequently accessed data (user profiles, popular posts)
- Moved image processing to a background job queue using Bull
- Implemented basic rate limiting to prevent abuse
- Added response caching for static content
// Quick Redis caching example
const getPopularPosts = async () => {
const cached = await redis.get('popular_posts');
if (cached) return JSON.parse(cached);
const posts = await db.query('SELECT * FROM posts ORDER BY likes DESC LIMIT 20');
await redis.setex('popular_posts', 300, JSON.stringify(posts)); // Cache for 5 minutes
return posts;
};
The Morning After
By 6 AM, the site was stable again. Response times were back to normal, users could log in, and new signups were flowing smoothly. I was exhausted but exhilarated.
We had gone from barely handling 100 users to comfortably serving 10,000, and eventually scaled to over 25,000 users with the same infrastructure.
Key Lessons Learned
1. Connection pooling is not optional Always implement proper database connection pooling from day one. It's not premature optimization - it's basic infrastructure.
2. Index your database properly
Foreign keys, frequently filtered columns, and ORDER BY columns should all have indexes. Use EXPLAIN ANALYZE to identify slow queries.
3. One query is better than N queries N+1 query problems are silent killers. They work fine in development and murder you in production.
4. Cache aggressively, invalidate intelligently Redis saved us. Cache read-heavy data and invalidate when it changes. A 5-minute cache TTL can reduce database load by 80%.
5. Monitor everything We had no monitoring in place. I set up basic logging and metrics that night, which helped us catch future issues before users did.
6. Plan for 10x growth If you have 100 users today, architect like you'll have 1,000 tomorrow. It's easier to scale good architecture than to rewrite bad architecture under pressure.
7. Background jobs for heavy lifting Never do heavy processing (image resizing, video encoding, email sending) in your API request cycle. Use job queues.
The Tools That Saved Us
- pg-pool: Proper PostgreSQL connection pooling
- Redis: Caching layer that reduced database load by 70%
- Bull: Reliable job queue for background processing
- PM2: Process manager that kept our Node.js app running and utilized all CPU cores
- Nginx: Reverse proxy for serving static files and load balancing
What I'd Do Differently
Looking back, here's what I wish we had done from the start:
- Load testing: We should have stress-tested our application before the launch
- Auto-scaling: Set up horizontal scaling policies in advance
- Database read replicas: For read-heavy workloads, replicas would have helped immensely
- Better monitoring: Implementing APM (Application Performance Monitoring) tools like New Relic or DataDog
- CDN for static assets: We eventually moved to a CDN, which took significant load off our servers
Final Thoughts
That all-night debugging session taught me more about scalability, databases, and system architecture than any tutorial ever could. When you're watching real users struggle with your application at 3 AM, you learn fast.
The startup world moves quickly, and sometimes you have to scale in real-time while keeping the plane in the air. It's stressful, exhausting, and absolutely thrilling.
If you're building something that might suddenly get popular, don't wait for the traffic spike to think about scalability. Start with good fundamentals - proper connection management, database indexes, caching strategy, and monitoring. Your future sleep-deprived self will thank you.
And if you do find yourself in a similar situation? Take a deep breath, grab some coffee, and remember: every problem has a solution. You just have to find it.
Have you experienced a similar scaling challenge? I'd love to hear your story in the comments below.
