Free
2026-09-11
ssrscalingmeeshoreact
20M users on SSR: the Meesho playbook (and what broke)
Meesho's journey from startup to 20M+ users provides critical insights into SSR scaling patterns and their breaking points.
# Architecture evolution
# Phase 1: Traditional SSR (React + Node.js)
Initial architecture followed Next.js patterns:
// Traditional SSR approach
export async function getServerSideProps(context) {
const data = await fetchUserData(context.params.id);
return { props: { data } };
}
export default function UserPage({ data }) {
return <UserProfile user={data} />;
}Challenges encountered:
- Cold start delays (~2-3 seconds)
- High memory usage per request
- Database connection pool exhaustion
# Phase 2: Hybrid SSR + SSG
Transition to static generation for critical pages:
// Hybrid approach
export async function getStaticPaths() {
return { paths: popularUsers.map(id => ({ params: { id } })) };
}
export async function getStaticProps({ params }) {
const data = await fetchUserData(params.id);
return { props: { data }, revalidate: 3600 };
}Benefits:
- Sub-100ms page loads
- Reduced server costs
- Better SEO
Remaining issues:
- Dynamic content staleness
- Build time explosion
# Phase 3: Edge SSR + Incremental Static Regeneration
Final architecture with edge computing:
// Edge SSR with ISR
export const config = { runtime: 'edge' };
export default async function EdgeUserPage({ params }) {
const data = await fetchUserData(params.id);
return <UserProfile user={data} />;
}# Critical failure points
# 1. Database scaling
Problem: Connection pool exhaustion at scale
// Initial approach (problematic)
const pool = new Pool({
max: 100, // Insufficient for 20M users
connectionTimeoutMillis: 10000,
});Solution: Connection pooling + read replicas
// Scaled solution
const pool = new Pool({
max: 500,
maxUses: 5000,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
// Read replicas for analytics
const analyticsPool = new Pool({
host: 'analytics-replica',
max: 200,
});# 2. Cache invalidation
Problem: Stale content serving
// Naive cache (problematic)
const cache = new Map();
function getCachedData(key) {
return cache.get(key);
}Solution: Multi-layer caching
// Scaled caching solution
const cache = {
memory: new LRUCache({ max: 10000 }),
redis: createRedisClient(),
cdn: createCDNClient(),
};# 3. Error handling at scale
Problem: Cascading failures
// Initial error handling (problematic)
try {
const data = await fetchData();
return { success: true, data };
} catch (error) {
return { success: false, error: error.message };
}Solution: Circuit breakers + graceful degradation
// Scaled error handling
const circuitBreaker = new CircuitBreaker(fetchData, {
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 30000,
});
// Fallback strategy
async function fetchDataWithFallback() {
try {
return await circuitBreaker.fire();
} catch (error) {
return await getStaleDataFromCache();
}
}# Performance metrics
| Metric | Initial | Final | Improvement |
|---|---|---|---|
| Page load time | 2.5s | 0.08s | 31x faster |
| Memory usage | 512MB | 128MB | 75% reduction |
| Error rate | 5% | 0.1% | 50x improvement |
| Cost per request | $0.001 | $0.0002 | 80% reduction |
# Lessons learned
# 1. Embrace edge computing
- Move compute closer to users
- Reduce cold start times
- Improve latency
# 2. Cache strategically
- Multi-layer caching (memory + distributed + CDN)
- Cache invalidation is as important as caching
- Stale content is better than no content
# 3. Monitor everything
- Real-time metrics at scale
- Alert on anomalies
- Automated scaling
# 4. Plan for failure
- Circuit breakers prevent cascading failures
- Graceful degradation maintains UX
- Failover systems are mandatory
This case study demonstrates that scaling SSR requires thinking beyond traditional patterns and embracing edge computing, sophisticated caching, and resilient architectures.