We've been called in to rescue a lot of MVPs. The pattern is almost always the same: the product launched fine, got traction, and then somewhere between 500 and 2,000 users, things started breaking. Response times climbed. The database started struggling. Features that worked fine at 50 users became unusable at 500. Here are the five architectural shortcuts we see most often — and what to do instead.
1. Synchronous Everything
The most common culprit
The MVP does everything in the HTTP request-response cycle: send a welcome email, resize an uploaded image, call three third-party APIs, write to the database, and return a response — all synchronously. At 10 users, this is fine. At 1,000 concurrent users, your API response times are 8 seconds and your server is timing out.
The fix is to identify which operations the user doesn't need to wait for and move them to a background job queue. Sending emails, processing uploads, generating reports, syncing to external systems — none of these need to block the HTTP response. A simple BullMQ queue backed by Redis handles this well and is easy to add to an existing Next.js or Node.js app.
The rule of thumb: if the user doesn't need the result of an operation to render the next screen, it should be async.
2. No Database Indexes on Foreign Keys
Silent until it isn't
PostgreSQL doesn't automatically create indexes on foreign key columns. This is fine when your tables have 1,000 rows. When they have 500,000 rows and you're doing a JOIN on an unindexed foreign key, that query goes from 2ms to 4 seconds.
We've seen this kill products that were otherwise well-architected. The fix is straightforward: add indexes on every foreign key column, and use EXPLAIN ANALYZE on your slow queries to find missing indexes. Do this before you have a performance problem, not after.
-- Add this to every migration that creates a foreign key
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders(user_id);
CREATE INDEX CONCURRENTLY idx_orders_tenant_id ON orders(tenant_id);
3. Fetching Everything, Filtering in Code
The N+1 problem and its cousins
A common pattern in ORMs: fetch all records, then filter in JavaScript/Python. const activeUsers = await User.findAll().filter(u => u.isActive). At 100 users, this is imperceptible. At 100,000 users, you're loading 100,000 rows into memory to return 50.
The related problem is N+1 queries: fetching a list of orders, then making a separate database query for each order to get the associated user. 100 orders = 101 database queries. Use JOINs or ORM eager loading to fetch related data in a single query.
The discipline to develop early: always push filtering, sorting, and pagination to the database layer. Never load more rows than you need to return.
4. No Caching Layer
Hitting the database for everything
Some data doesn't change often: user profile data, product catalogue, configuration settings, permission lists. Fetching these from the database on every request is wasteful and adds latency. At scale, it's a significant fraction of your database load.
Redis is the standard answer. Cache user sessions, frequently-read configuration, and expensive computed results with a TTL that matches how often the data changes. For Next.js apps, the built-in fetch cache and ISR handle a lot of this at the edge without needing Redis at all.
The trap to avoid: caching mutable data without a proper invalidation strategy. Cache invalidation is famously hard. Start with read-heavy, rarely-changing data and expand from there.
5. Single-Server Everything
The single point of failure
The MVP runs on a single EC2 instance or a single Heroku dyno. The database is on the same server. There's no load balancer. When that server goes down — and it will — the product is down. When traffic spikes, the server runs out of memory and starts swapping.
You don't need a Kubernetes cluster to fix this. The minimum viable production setup is: a managed database (RDS, PlanetScale, Supabase) separate from your application server, at least two application instances behind a load balancer, and a CDN in front of your static assets. This setup handles most traffic spikes and survives single-instance failures.
The cost difference between a single-server setup and this minimum viable HA setup is usually $50–150/month. It's worth it before you have paying customers, not after your first outage.
The Common Thread
None of these problems require you to over-engineer your MVP. You don't need microservices, event sourcing, or a distributed cache cluster on day one. But you do need to make a handful of decisions correctly from the start: push filtering to the database, index your foreign keys, separate your database from your application server, and identify which operations should be async.
The MVPs that scale well aren't the ones with the most sophisticated architecture — they're the ones that avoided the five mistakes above. Everything else can be refactored. These can't, at least not without significant pain.
Building an MVP and want to avoid these pitfalls from the start? We do architecture reviews and can help you make the right calls early.