HinterBuild logoHinterBuild
Backend Systems · 9 min read

System Design for 10M Users: Practical Architecture Guide

Learn system design for 10m users through concrete architecture trade-offs, failure modes, rollout controls, and production measurement practices.

Muhammad Abdul Sami, author

Muhammad Abdul Sami

· Updated · 9 min read

  • RAG
  • Embeddings
  • Vector Databases
  • Evaluation

Table of Contents:

What 10M Users Actually Means

Short answer: System design for 10M users requires architecting for roughly 500–2,000 requests per second peak, terabytes of storage, and failure of any single component without user-visible downtime — not just "add more servers."

If you searched "system design 10 million users", you are preparing for scale that breaks naive architectures: single PostgreSQL instances, synchronous email sends, unbounded session state, and missing cache layers. At HinterBuild, we walk through this exact progression with every backend API engineering client before they hit growth inflection points.

Key Takeaways:

  • 10M registered users ≈ 1–2M DAU500–2K RPS peak for typical SaaS
  • Stateless API servers + Redis sessions enable horizontal scaling
  • Read replicas handle 80% of database load; sharding comes at ~50M+ rows per hot table
  • Async queues decouple write-heavy operations (email, analytics, search indexing)
  • Design for failure — any component must be replaceable without downtime

This guide walks through system design for 10M users step by step, with concrete numbers, technology choices, and code patterns used in production 2026 architectures.


Step 1: Estimate Traffic and Storage

Before drawing boxes, quantify the problem. Interviewers and architects both start here.

User Assumptions (Typical B2B SaaS)

MetricConservativeAggressive
Registered users10M10M
Daily active users (DAU)10% = 1M20% = 2M
Requests per DAU per day50200
Peak/average traffic ratio10×

Calculate Peak RPS

Daily requests = DAU × requests per user
               = 1,000,000 × 50 = 50M requests/day

Average RPS = 50M / 86,400 ≈ 580 RPS

Peak RPS = 580 × 5 ≈ 2,900 RPS

Plan infrastructure for 3,000 RPS peak with 2× headroom → design target 6,000 RPS capacity.

Storage Estimates

EntityRows (10M users)Avg row sizeTotal
Users10M2 KB20 GB
Sessions/events (1yr)500M500 B250 GB
User-generated content50M5 KB250 GB
Total (year 1)~520 GB

Add 3× for indexes, replicas, and backups → ~1.5 TB PostgreSQL footprint. Manageable on a well-tuned single primary with read replicas — sharding not yet required.

Compare runtime efficiency in our Go vs Python backend benchmarks when sizing API pods.


Step 2: Draw the High-Level Architecture

At 10M users, your architecture has distinct layers:

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   Clients    │────▶│  CDN / WAF   │────▶│ Load Balancer│
│ Web + Mobile │     │  (static)    │     │   (ALB/NLB)  │
└──────────────┘     └──────────────┘     └──────┬───────┘
                                                  │
                    ┌─────────────────────────────┼─────────────────────────────┐
                    ▼                             ▼                             ▼
              ┌──────────┐                 ┌──────────┐                 ┌──────────┐
              │ API Pod  │                 │ API Pod  │                 │ API Pod  │
              │ (stateless)               │ (stateless)               │ (stateless)
              └────┬─────┘                 └────┬─────┘                 └────┬─────┘
                   │                            │                            │
         ┌─────────┼────────────────────────────┼────────────────────────────┼─────────┐
         ▼         ▼                            ▼                            ▼         ▼
   ┌─────────┐ ┌─────────┐              ┌──────────┐              ┌──────────────┐
   │  Redis  │ │ Postgres│              │  SQS/SNS │              │ Elasticsearch│
   │ (cache) │ │ primary │              │ (queues) │              │ (search)     │
   └─────────┘ └────┬────┘              └──────────┘              └──────────────┘
                    │
              ┌─────┴─────┐
              ▼           ▼
        ┌──────────┐ ┌──────────┐
        │ Replica 1│ │ Replica 2│
        └──────────┘ └──────────┘

Deploy this on cloud infrastructure with multi-AZ redundancy. Each layer scales independently.


Step 3: Load Balancing and Stateless Services

Stateless API servers are the foundation of horizontal scaling. No session data in memory, no local file storage, no sticky requirements unless unavoidable.

Stateless API Checklist

  • Sessions stored in Redis, not server memory
  • File uploads go directly to S3 via presigned URLs
  • No in-process caches that can't be invalidated cluster-wide
  • Idempotent handlers for retry safety
  • Health checks on /health and /ready (DB + Redis connectivity)

Load Balancer Configuration

SettingValueReason
AlgorithmRound robin or least connectionsEven distribution
Health check interval10sFast unhealthy detection
Deregistration delay30sDrain in-flight requests
Idle timeout120sSupport streaming responses
TLS terminationAt LBCentralized cert management

Auto-Scaling Rules

typescript
// Example: AWS Auto Scaling policy (conceptual)
const scalingPolicy = {
  metric: "CPUUtilization",
  targetValue: 60,
  scaleOutCooldown: 60,   // seconds
  scaleInCooldown: 300,
  minCapacity: 3,         // multi-AZ minimum
  maxCapacity: 30,
};

Start with 3 API pods minimum (one per AZ). Scale out at 60% CPU, scale in slowly to avoid flapping. At 3,000 RPS with 500 RPS per pod, you need 6–8 pods at peak — size pods based on your framework benchmarks.


Step 4: Caching Strategy (CDN, Redis, Application)

Caching is the highest-ROI optimization in system design for 10M users. A three-tier cache eliminates 80%+ of database reads.

Three-Tier Cache Architecture

TierTechnologyTTLUse Case
L1: CDNCloudFront/Fastly1h–24hStatic assets, public API responses
L2: RedisElastiCache/MemoryDB1m–1hSession, user profile, hot lists
L3: ApplicationIn-process LRU10s–60sConfig, feature flags, reference data

Redis Cache-Aside Pattern

python
import redis.asyncio as redis
import json

cache = redis.from_url("redis://cache-cluster:6379")

async def get_user(user_id: str) -> dict:
    cache_key = f"user:{user_id}"
    cached = await cache.get(cache_key)
    if cached:
        return json.loads(cached)

    # Cache miss → database
    user = await db.fetch_user(user_id)
    await cache.setex(cache_key, 300, json.dumps(user))  # 5 min TTL
    return user

Go equivalent:

go
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
    key := "user:" + id

    if val, err := s.redis.Get(ctx, key).Bytes(); err == nil {
        var u User
        json.Unmarshal(val, &u)
        return &u, nil
    }

    u, err := s.repo.FindByID(ctx, id)
    if err != nil {
        return nil, err
    }

    data, _ := json.Marshal(u)
    s.redis.SetEx(ctx, key, data, 5*time.Minute)
    return u, nil
}

Cache Invalidation Rules

  • Write-through for critical data (user permissions) — update cache on write
  • TTL-based for eventually consistent data (analytics dashboards)
  • Pub/sub invalidation for cluster-wide L3 bust: publish invalidate:user:123 on write

Avoid caching personalized responses at CDN unless keyed by user — use Redis instead.


Step 5: Database Scaling (Read Replicas, Sharding)

At 10M users, PostgreSQL with read replicas handles most workloads. Sharding is a future problem unless you have a single hot table exceeding 50M rows.

Read Replica Strategy

Query TypeRoute ToExample
WritesPrimaryINSERT, UPDATE, DELETE
Reads (tolerant of lag)ReplicaList pages, search, dashboards
Reads (must be fresh)PrimaryPost-login permissions, billing
typescript
// Read/write routing in application layer
async function getDbConnection(mode: "read" | "write") {
  const url = mode === "write"
    ? process.env.DATABASE_PRIMARY_URL
    : process.env.DATABASE_REPLICA_URL;
  return pool.connect(url);
}

Typical replica lag: 10–100ms. Acceptable for 95% of reads. Route payment and auth checks to primary.

Connection Pooling

Never connect API pods directly to PostgreSQL at scale. Use PgBouncer:

SettingRecommendedWhy
Pool modeTransactionBest for web APIs
Max connections (Postgres)200–500RAM-limited
Pool size per pod20–50Prevent connection storms

See PostgreSQL performance secrets for index and query tuning that matters more than replicas.

When to Shard (Not Yet at 10M)

Shard when:

  • Single table exceeds 50–100M rows with hot access patterns
  • Write throughput exceeds primary capacity (> 10K writes/sec)
  • Replica lag consistently exceeds 500ms under normal load

At 10M users, partitioning by time (events table) often suffices before full sharding:

sql
CREATE TABLE events (
    id          BIGSERIAL,
    user_id     UUID NOT NULL,
    event_type  TEXT NOT NULL,
    payload     JSONB,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
) PARTITION BY RANGE (created_at);

CREATE TABLE events_2026_09 PARTITION OF events
    FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');

Step 6: Async Processing with Message Queues

Synchronous request handlers must not perform slow operations. At 3,000 RPS, sending email, indexing search, or calling ML models inline will collapse your API.

Queue Architecture

API Handler ──▶ Write to DB ──▶ Publish event ──▶ Return 202/200
                                      │
                    ┌─────────────────┼─────────────────┐
                    ▼                 ▼                 ▼
              Email Worker    Search Indexer    Analytics Writer
OperationSync or AsyncQueue
User signupSync (create record) + Async (welcome email)SQS/SNS
Search indexingAsyncKafka/SQS
Analytics eventsAsync (batch)Kinesis/Kafka
Payment webhooksSync (validate) + Async (fulfillment)SQS
LLM inferenceAsync or streamingDedicated queue

Idempotent Worker Pattern

python
from dataclasses import dataclass
import hashlib

@dataclass
class Event:
    id: str
    type: str
    payload: dict

async def process_event(event: Event):
    # Idempotency key prevents duplicate processing
    idempotency_key = hashlib.sha256(
        f"{event.type}:{event.id}".encode()
    ).hexdigest()

    if await redis.setnx(f"processed:{idempotency_key}", "1"):
        await redis.expire(f"processed:{idempotency_key}", 86400)
        await handle_event(event)
    else:
        logger.info("Duplicate event skipped", event_id=event.id)

Build reliable data pipelines with dead-letter queues (DLQ) for failed messages. Monitor DLQ depth — sustained growth indicates a systemic failure.


Step 7: Observability and SLOs

You cannot scale what you cannot measure. System design for 10M users requires SLOs, not just dashboards.

Define SLOs First

SLISLO TargetMeasurement
Availability99.9% (8.7h downtime/yr)Successful requests / total
Latency (p95)< 200msAPI gateway metrics
Latency (p99)< 500msAPI gateway metrics
Error rate< 0.1%5xx / total requests
Queue processing< 30s lagConsumer offset lag

Three Pillars Implementation

PillarTool (example)What to Track
MetricsPrometheus + GrafanaRPS, latency histograms, saturation
LogsStructured JSON → Loki/CloudWatchRequest ID, user ID, error context
TracesOpenTelemetry → Tempo/JaegerCross-service latency breakdown
typescript
// OpenTelemetry trace propagation (TypeScript API gateway)
import { trace, context } from "@opentelemetry/api";

app.use(async (req, res, next) => {
  const span = trace.getTracer("api").startSpan(`${req.method} ${req.path}`);
  req.span = span;
  res.on("finish", () => span.end());
  context.with(trace.setSpan(context.active(), span), next);
});

Our observability & monitoring practice deploys these stacks with alert runbooks tied to SLO burn rates — not just "CPU > 80%."

Alerting Hierarchy

  1. Page on-call: SLO burn rate > 10× (potential outage)
  2. Ticket: Error rate elevated but SLO intact
  3. Dashboard only: Capacity trending toward limits (plan scaling)

Step 8: Failure Modes and Resilience

At 10M users, failures are daily events — design for them.

Common Failure Modes

FailureImpactMitigation
Single API pod crashNone (LB routes around)Multi-AZ, health checks
Redis cluster failover1–3s cache miss spikeRedis Sentinel/Cluster, graceful degradation
Primary DB failureWrite outageAutomated failover (Patroni/RDS Multi-AZ)
Replica lag spikeStale readsRoute critical reads to primary
Queue backlogDelayed emails/notificationsAuto-scale workers, DLQ alerts
Third-party API downFeature degradedCircuit breaker, cached fallback

Circuit Breaker Pattern

go
import "github.com/sony/gobreaker"

var cb = gobreaker.NewCircuitBreaker(gobreaker.Settings{
    Name:        "payment-api",
    MaxRequests: 3,
    Interval:    10 * time.Second,
    Timeout:     30 * time.Second,
    ReadyToTrip: func(counts gobreaker.Counts) bool {
        return counts.ConsecutiveFailures > 5
    },
})

func chargePayment(ctx context.Context, req PaymentRequest) error {
    _, err := cb.Execute(func() (interface{}, error) {
        return nil, paymentClient.Charge(ctx, req)
    })
    return err
}

Graceful Degradation

When Redis is down: skip cache, serve from database (slower but functional). When search index lags: return database results with a "results may be stale" flag. When ML service is down: disable AI features, core product still works.

Avoid API design mistakes that amplify failures — unbounded queries and missing timeouts turn partial outages into full ones.


Complete Architecture Diagram

Final system design for 10M users component list:

ComponentTechnologyCount (peak)Purpose
CDNCloudFrontGlobalStatic assets, edge cache
WAFAWS WAF1DDoS, bot protection
Load BalancerALBMulti-AZTraffic distribution
API ServersGo/FastAPI6–12 podsStateless business logic
RedisElastiCache3-node clusterCache + sessions
PostgreSQLRDS/Aurora1 primary + 2 replicasPersistent data
PgBouncerSidecar/containerPer AZConnection pooling
Message QueueSQS/KafkaManagedAsync processing
WorkersPython/Go4–8 podsEmail, search, analytics
SearchElasticsearch3-nodeFull-text search
Object StorageS3Files, backups
MonitoringPrometheus/GrafanaManagedSLOs, alerting

Estimated monthly cost (AWS, 2026): $8,000–$15,000 depending on data transfer and reserved instance commitments. Optimize with spot instances for workers and reserved capacity for database.

Capacity Planning Spreadsheet

Use this template when presenting system design for 10M users to stakeholders:

ComponentUnit capacityPeak needInstancesHeadroom
API pod500 RPS3,000 RPS6 (+ 2 spare)33%
Redis node100K ops/s30K ops/s3 (cluster)70%
Postgres primary5K writes/s200 writes/s195%
Postgres replica20K reads/s2,800 reads/s230%
Worker pod500 events/s800 events/s425%

Headroom below 30% on any tier triggers scale planning — not emergency firefighting. Review quarterly as user growth compounds.

Choose API runtime using framework benchmarks to calibrate "unit capacity" for your specific endpoints rather than generic hello-world numbers.


Primary references: official documentation, official documentation, official documentation, official documentation.

Frequently Asked Questions

How many servers do I need for 10M users?

For typical SaaS at 3,000 RPS peak: 6–12 API pods, 3-node Redis cluster, 1 PostgreSQL primary + 2 replicas, and 4–8 async workers. Exact sizing depends on your framework and runtime.

Do I need microservices at 10M users?

No. A well-structured modular monolith with async workers handles 10M users for most products. Split services when team size (> 20 engineers) or independent scaling requirements force it — not preemptively.

When should I shard my database?

When a single table exceeds 50–100M rows with performance degradation despite indexing, or write throughput exceeds primary capacity. At 10M users with good schema design, read replicas and partitioning usually suffice.

What is the most common scaling mistake?

Skipping caching. A Redis layer eliminates 80% of database load. The second most common: synchronous slow operations (email, search indexing) in request handlers.

How do I handle 10M WebSocket connections?

Dedicated connection servers (often Go), regional edge deployment, and Redis pub/sub for cross-node messaging. See streaming LLM production patterns for SSE/WebSocket architecture.

What SLO should I target at 10M users?

99.9% availability and p95 < 200ms are standard for B2B SaaS. Consumer apps may need 99.95%+. Define SLOs before choosing infrastructure — they drive every scaling decision.

Should I use Kubernetes at this scale?

Optional. Managed container services (ECS, Cloud Run) reduce operational overhead. Kubernetes makes sense with dedicated platform teams or multi-cloud requirements.

How do I practice system design interviews for 10M users?

Follow this eight-step framework: estimate traffic → draw architecture → load balance → cache → database → queues → observability → failure modes. Justify every component with numbers.


Conclusion

System design for 10M users is achievable with a disciplined progression: estimate load, keep API servers stateless, cache aggressively, replicate databases, async slow work, and observe everything.

  • Start with numbers — RPS, storage, peak ratios
  • Cache at CDN, Redis, and application layers
  • Read replicas before sharding
  • Queues for everything that isn't user-facing latency
  • Design for failure — circuit breakers, graceful degradation, SLOs

At HinterBuild, we architect scalable systems from MVP to 10M+ users:

Schedule a consultation to review your architecture before scale breaks it.

Free consultation

Book a free consultation call on system design & scalable architecture

30-minute call with the HinterBuild team. Discuss your project, architecture questions, or next steps — no obligation.

Book a meeting

Keep reading