Scaling Your API: Part 1 - Performance & Infrastructure
π This is Part 1 of the βScaling Your APIβ Series
- Part 1 (this page): Performance & Infrastructure - Technical techniques to handle millions of requests
- Part 2: Design & Architecture β - Organizational strategies and API design patterns for large-scale systems
- Part 3: Choosing the Right Database β - Database selection for your API
- Part 4: Load Balancing & High Availability β - Keeping your API always available
- Part 5: Monitoring & Performance β - Tracking and improving API performance
The Journey: 1 RPS β 1,000,000 RPS
Imagine your API is a restaurant. At first, you have one chef (server) handling one order at a time. But what happens when you go viral and suddenly have a million customers waiting?
of Users"] --> LB["βοΈ Load
Balancer"] LB --> S2["π₯οΈ Server 1"] LB --> S3["π₯οΈ Server 2"] LB --> S4["π₯οΈ Server N..."] S2 --> C["β‘ Cache"] S3 --> C S4 --> C C --> DB["ποΈ Database
Cluster"] end
Overview: The 14 Key Scaling Techniques
Scaling an API isnβt about implementing all techniques at onceβitβs about choosing the right tools for your current stage. This guide covers 14 proven techniques in order of complexity:
Foundation (Must-Haves for Every API)
- Vertical Scaling - Start here: upgrade your single serverβs resources
- Connection Pooling - Reuse database connections instead of opening new ones
- Avoid N+1 Queries - Fetch related data efficiently with joins
Essential Optimizations (1-10K RPS)
- Horizontal Scaling - Add more servers to distribute the load
- Load Balancing - Intelligently route traffic across servers
- Caching - Store frequently accessed data in memory (Redis/Memcached)
- Pagination - Send data in manageable chunks instead of all at once
Performance Enhancements (10K-100K RPS)
- Fast JSON Serializers - Use optimized libraries for data serialization
- Compression - Reduce payload sizes with Gzip or Brotli
- CDN - Serve content from edge servers close to users
- Async Processing - Move slow tasks to background workers
Advanced Scaling (100K+ RPS)
- Async Logging - Make logging non-blocking
- Database Sharding - Split your database horizontally
- Rate Limiting - Protect your system from abuse
The Three Critical Insights
1. Start Simple, Scale Progressively Donβt build for 1M RPS on day one. A single well-optimized server can handle 10,000+ RPS with proper caching and database optimization.
2. Fix the Bottleneck, Not Everything Measure your system to identify the actual bottleneck (CPU? Memory? Database? Network?) and address that specific issue first.
3. Some Techniques Are Always Worth It Connection pooling, avoiding N+1 queries, basic caching, pagination, and compression should be implemented from the startβtheyβre simple and have massive impact.
Letβs dive into each technique in detail, starting from the simplest to the most advanced.
Level 1: Vertical Scaling (The Easy Start)
What is it?
Make your single server bigger and more powerful β more CPU, more RAM, faster disk.
2 CPU, 4GB RAM"] end subgraph after["After"] B["π₯οΈ Big Server
32 CPU, 128GB RAM"] end before -->|π° Upgrade| after style A fill:#fecaca,stroke:#dc2626 style B fill:#d1fae5,stroke:#059669
Real-World Analogy
Instead of hiring more chefs, you give your one chef a bigger kitchen with better equipment.
Limits
- β Thereβs a ceiling β servers can only get so big
- β Single point of failure β if it crashes, everything goes down
- β Expensive at the top end
Good for: 1 β 100 RPS
Level 2: Horizontal Scaling (Add More Servers)
What is it?
Instead of one big server, use many smaller servers working together.
Real-World Analogy
Hire more chefs! Each chef can work independently, handling their own orders.
Benefits
- β No ceiling β just add more servers
- β No single point of failure β if one dies, others keep working
- β Cost-effective β use cheaper commodity hardware
Good for: 100 β 10,000 RPS
Level 3: Load Balancing (Traffic Cop)
What is it?
A load balancer distributes incoming requests across multiple servers evenly.
Common Algorithms
| Algorithm | How it Works | Best For |
|---|---|---|
| Round Robin | Takes turns: 1β2β3β1β2β3 | Equal server capacity |
| Least Connections | Sends to server with fewest active requests | Varying request times |
| IP Hash | Same user always goes to same server | Session persistence |
| Weighted | Stronger servers get more traffic | Mixed server sizes |
Real-World Analogy
A host at a restaurant who seats customers at different tables to keep all waiters equally busy.
Level 4: Database Connection Pooling
What is it?
Instead of opening a new database connection for every request, maintain a pool of ready-to-use connections that can be reused.
Why It Matters
Opening a database connection is expensive:
| Action | Time |
|---|---|
| Open new connection | 50-100ms |
| Reuse from pool | < 1ms |
With 1000 requests/sec, you save 50-100 seconds of work!
Without vs With Connection Pooling
50ms"] O1 --> Q1["Query
10ms"] Q1 --> C1["Close connection
5ms"] C1 --> T1["Total: 65ms"] end subgraph with["β With Pooling (Fast)"] direction TB S2["Request"] --> G2["Get from pool
< 1ms"] G2 --> Q2["Query
10ms"] Q2 --> R2["Return to pool
< 1ms"] R2 --> T2["Total: 12ms"] end style without fill:#fecaca,stroke:#dc2626 style with fill:#d1fae5,stroke:#059669
Real-World Analogy
Instead of getting a new fork for every bite of food, you keep your fork and reuse it throughout the meal.
Implementation Tips
# Python example with connection pooling
from sqlalchemy import create_engine
# Create engine with connection pool
engine = create_engine(
'postgresql://user:pass@localhost/db',
pool_size=20, # Keep 20 connections ready
max_overflow=10, # Allow 10 more if needed
pool_pre_ping=True # Check if connection is alive
)
Good for: All production APIs with databases
Level 5: Avoiding N+1 Query Problems
What is it?
The N+1 problem happens when you fetch a list of items, then make a separate query for each itemβs related data. This turns 1 query into N+1 queries!
The Better Way: Join or Batch Queries
(using JOIN) DB-->>A: All data in one result Note over A,DB: Total: 1 query, 50ms
Problem Example
# β BAD: N+1 Problem
users = User.query.all() # 1 query
for user in users: # 100 users
posts = user.posts # 100 more queries!
# Total: 101 queries
Solution Example
# β
GOOD: Eager Loading
users = User.query.options(
joinedload(User.posts)
).all() # Just 1 query with JOIN!
for user in users:
posts = user.posts # No additional query!
Performance Impact
Real-World Analogy
Instead of making 100 trips to the store to buy 100 items, you make one trip with a shopping list and get everything at once.
Good for: Any API that returns lists with related data
Level 6: Caching (Remember & Reuse)
What is it?
Store frequently accessed data in fast memory so you donβt have to compute or fetch it again.
Types of Caching
Client-side"] CDN["π‘ CDN Cache
Edge servers"] APP["β‘ App Cache
Redis/Memcached"] DB["ποΈ DB Cache
Query cache"] end B --> CDN --> APP --> DB style B fill:#dbeafe,stroke:#2563eb style CDN fill:#d1fae5,stroke:#059669 style APP fill:#fef3c7,stroke:#d97706 style DB fill:#fce7f3,stroke:#db2777
What to Cache
- β API responses that donβt change often
- β Database query results
- β Session data
- β Computed values (totals, aggregations)
Real-World Analogy
A chef who preps ingredients in advance. Instead of chopping onions for every order, they chop a big batch and grab from it.
Good for: 1,000 β 100,000 RPS
Level 7: Pagination (Send Data in Chunks)
What is it?
Instead of returning thousands of records at once, break them into small pages that load quickly.
LIMIT 20 OFFSET 0 D-->>A: 20 records (10KB) A-->>C: Response takes 50ms π
Common Pagination Approaches
Implementation
// Offset-based pagination (simple but slower for large offsets)
GET /api/users?page=1&limit=20
// Cursor-based pagination (faster, handles real-time changes)
GET /api/users?cursor=abc123&limit=20
Response Format
{
"data": [...],
"pagination": {
"total": 10000,
"page": 1,
"limit": 20,
"total_pages": 500,
"next_cursor": "xyz789"
}
}
Real-World Analogy
Instead of downloading an entire encyclopedia, you download one page at a time as you read it.
Good for: Any endpoint that returns lists or collections
Level 8: Lightweight JSON Serializers
What is it?
JSON serialization (converting data structures to JSON) can be a bottleneck. Fast serializers can be 2-10x faster than default ones.
Performance Comparison
| Language | Default | Fast Library | Speedup |
|---|---|---|---|
| Python | json |
orjson or ujson |
5-10x |
| Node.js | JSON.stringify() |
fast-json-stringify |
2-3x |
| Java | Jackson |
Jackson (already fast!) |
- |
| Go | encoding/json |
jsoniter |
3-4x |
Example Implementation
# β Default (slower)
import json
response = json.dumps(data)
# β
Fast serializer
import orjson
response = orjson.dumps(data) # 5-10x faster!
// β Default (slower)
const json = JSON.stringify(data);
// β
Fast serializer with schema
const fastJson = require('fast-json-stringify');
const stringify = fastJson({
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'integer' }
}
});
const json = stringify(data); // 2-3x faster!
Real-World Analogy
Using a high-speed printer instead of an old dot-matrix printer to print the same document.
Good for: High-traffic APIs with large response payloads
Level 9: Compression (Shrink the Data)
What is it?
Compress API responses before sending them over the network, reducing bandwidth and transfer time.
Accept-Encoding: gzip S-->>C: 100KB compressed (fast!) Note over C: Download time: 0.8 seconds π
Compression Algorithms
| Algorithm | Compression | Speed | Browser Support |
|---|---|---|---|
| Gzip | Good (70-80%) | Fast | β Universal |
| Brotli | Better (75-85%) | Medium | β Modern browsers |
| Deflate | Good (70-80%) | Fast | β Universal |
When to Use Compression
- β Text responses (JSON, HTML, CSS, JS)
- β Responses larger than 1KB
- β Already compressed data (images, videos)
- β Very small responses (< 1KB)
Implementation
// Express.js (Node.js)
const compression = require('compression');
app.use(compression()); // Automatically compress all responses
# Flask (Python)
from flask_compress import Compress
Compress(app) # Enable compression
# Nginx configuration
gzip on;
gzip_types application/json text/css application/javascript;
gzip_min_length 1000;
Real-World Analogy
Vacuum-sealing clothes before packing them in a suitcase β same clothes, much less space.
Good for: All text-based API responses
Level 10: Content Delivery Network (CDN)
What is it?
A network of servers spread around the world that cache your content closer to users.
(New York)"] O["π₯οΈ Main Server"] end subgraph cdn["π CDN Edge Servers"] E1["π‘ London"] E2["π‘ Tokyo"] E3["π‘ Sydney"] E4["π‘ SΓ£o Paulo"] end O --> E1 O --> E2 O --> E3 O --> E4 U1["π€ UK User"] --> E1 U2["π€ Japan User"] --> E2 U3["π€ Australia User"] --> E3 U4["π€ Brazil User"] --> E4 style O fill:#dbeafe,stroke:#2563eb style E1 fill:#d1fae5,stroke:#059669 style E2 fill:#d1fae5,stroke:#059669 style E3 fill:#d1fae5,stroke:#059669 style E4 fill:#d1fae5,stroke:#059669
Without CDN vs With CDN
| Β | Without CDN | With CDN |
|---|---|---|
| User in Tokyo | Request travels to New York (200ms) | Request goes to Tokyo edge (20ms) |
| Server Load | Every request hits your server | Most requests served by CDN |
| Bandwidth Cost | You pay for all traffic | CDN handles most traffic |
Real-World Analogy
Instead of one restaurant in New York, you open franchises worldwide. Customers get the same food from their local branch.
Best For
- Static assets (images, CSS, JS)
- API responses that donβt change per user
- Video streaming
Level 11: Asynchronous Processing
What is it?
For slow operations, accept the request immediately and process it in the background.
Common Use Cases
| Operation | Sync Time | Async Benefit |
|---|---|---|
| Send email | 2-5 seconds | Instant response |
| Generate PDF | 10+ seconds | User doesnβt wait |
| Process payment | 3-5 seconds | Faster checkout |
| Resize image | 5-15 seconds | Upload feels instant |
Tools
- Message Queues: RabbitMQ, Amazon SQS, Redis
- Task Processors: Celery (Python), Sidekiq (Ruby), Bull (Node.js)
Real-World Analogy
At a busy restaurant, the waiter takes your order and immediately moves to the next table. The kitchen processes orders in the background.
Level 12: Asynchronous Logging
What is it?
Logging can slow down your API if it writes to disk synchronously. Async logging writes to a buffer first, then a background thread handles the disk writes.
Performance Impact
Implementation
# Python with async logging
import logging
from logging.handlers import QueueHandler, QueueListener
from queue import Queue
# Create a queue for async logging
log_queue = Queue()
# Main thread uses QueueHandler (non-blocking)
handler = QueueHandler(log_queue)
logger = logging.getLogger()
logger.addHandler(handler)
# Background thread processes the queue
listener = QueueListener(log_queue, logging.FileHandler('app.log'))
listener.start()
# Now logging is async!
logger.info('This is non-blocking') # < 1ms
// Node.js with async logging (pino)
const pino = require('pino');
const logger = pino({
transport: {
target: 'pino/file',
options: { destination: 'app.log' }
}
});
// Logs are buffered and written asynchronously
logger.info('This is non-blocking');
Real-World Analogy
Instead of stopping to write down every detail in a notebook, you jot quick notes on sticky notes and organize them into the notebook later.
Good for: High-traffic APIs with extensive logging
Level 13: Database Sharding (Split Your Data)
What is it?
Divide your database into smaller pieces (shards), each handling a portion of the data.
100M users"] end subgraph after["β Sharded Database"] R["π Router"] R --> S1["ποΈ Shard 1
Users A-H"] R --> S2["ποΈ Shard 2
Users I-P"] R --> S3["ποΈ Shard 3
Users Q-Z"] end style DB1 fill:#fecaca,stroke:#dc2626 style R fill:#fef3c7,stroke:#d97706 style S1 fill:#d1fae5,stroke:#059669 style S2 fill:#d1fae5,stroke:#059669 style S3 fill:#d1fae5,stroke:#059669
Sharding Strategies
Trade-offs
| Benefit | Challenge |
|---|---|
| β Each shard handles less load | β Cross-shard queries are complex |
| β Can scale horizontally | β Rebalancing shards is hard |
| β Failure affects only one shard | β Application logic becomes complex |
Real-World Analogy
Instead of one giant warehouse, you have regional warehouses. Each handles orders for their region.
Good for: 100,000 β 1,000,000+ RPS
Level 14: Rate Limiting (Protect Your System)
What is it?
Limit how many requests a user or client can make in a given time period.
Common Algorithms
| Algorithm | How it Works | Visual |
|---|---|---|
| Token Bucket | Tokens added at fixed rate; request consumes a token | πͺ£ Bucket fills up over time |
| Sliding Window | Count requests in rolling time window | π Moving window of time |
| Fixed Window | Reset counter at fixed intervals | β° Reset every minute |
Rate Limit Headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 67
X-RateLimit-Reset: 1640995200
Real-World Analogy
A bouncer at a club who only lets in 100 people per hour, no matter how long the line is.
The Complete Picture
Hereβs how all these techniques work together at scale:
Fast JSON"] S2["π₯οΈ Server
Async Logging"] S3["π₯οΈ Server
Connection Pool"] end subgraph cache["Cache Layer"] RC["β‘ Redis Cache"] end subgraph async["Async Layer"] Q["π¬ Queue"] W["βοΈ Workers"] end subgraph data["Data Layer
(Optimized Queries)"] SH1["ποΈ Shard 1"] SH2["ποΈ Shard 2"] SH3["ποΈ Shard 3"] end U1 --> CDN U2 --> CDN U3 --> CDN CDN --> RL RL --> LB LB --> S1 LB --> S2 LB --> S3 S1 --> RC S2 --> RC S3 --> RC S1 --> Q RC --> SH1 RC --> SH2 RC --> SH3 Q --> W W --> SH1 style CDN fill:#d1fae5,stroke:#059669 style RL fill:#fef3c7,stroke:#d97706 style LB fill:#dbeafe,stroke:#2563eb style RC fill:#fce7f3,stroke:#db2777 style Q fill:#e0e7ff,stroke:#6366f1
Scaling Roadmap
| RPS Target | Techniques to Add |
|---|---|
| 1 β 100 | Vertical scaling, connection pooling, avoid N+1 queries |
| 100 β 1K | Caching (Redis), pagination, compression |
| 1K β 10K | Horizontal scaling, load balancer, fast JSON serializers |
| 10K β 100K | CDN, async processing, async logging |
| 100K β 1M | Database sharding, rate limiting, microservices |
| 1M+ | Multi-region deployment, edge computing |
Quick Reference: What to Use When
(horizontal scaling)"] MEM --> MEM_S["Add caching
(Redis/Memcached)"] DB --> DB_S["Connection pooling,
avoid N+1, sharding"] NET --> NET_S["CDN, compression,
pagination"] LOG --> LOG_S["Async logging"] style Q fill:#f1f5f9,stroke:#64748b style CPU_S fill:#d1fae5,stroke:#059669 style MEM_S fill:#d1fae5,stroke:#059669 style DB_S fill:#d1fae5,stroke:#059669 style NET_S fill:#d1fae5,stroke:#059669 style LOG_S fill:#d1fae5,stroke:#059669
Complete Summary
| Technique | What It Does | Impact | When to Use |
|---|---|---|---|
| Vertical Scaling | Bigger server | Quick wins | Start here |
| Horizontal Scaling | More servers | High | Growing traffic |
| Load Balancing | Distribute traffic | High | Multiple servers |
| Connection Pooling | Reuse DB connections | Very High | Always! |
| Avoid N+1 Queries | Optimize DB queries | Very High | Always! |
| Caching | Remember results | Very High | Repeated queries |
| Pagination | Send data in chunks | High | Large datasets |
| Fast JSON Serializers | Faster responses | Medium | High traffic |
| Compression | Smaller payloads | High | Text responses |
| CDN | Serve from edge | High | Global users |
| Async Processing | Background jobs | High | Slow operations |
| Async Logging | Non-blocking logs | Medium | Heavy logging |
| Database Sharding | Split data | Very High | Massive datasets |
| Rate Limiting | Protect system | Medium | Always! |
Bringing It All Together: Your Scaling Journey
Scaling from 1 to 1 million RPS is not a single leapβitβs a series of strategic steps. Hereβs how to think about your progression:
Phase 1: Foundation (1-1K RPS)
Focus: Get the basics right
- β Start with vertical scaling (bigger server)
- β Implement connection pooling immediately
- β Fix N+1 query problems
- β Add basic caching for read-heavy operations
- β Enable compression
Mindset: At this stage, a single well-configured server is sufficient. Donβt over-engineer.
Phase 2: Growth (1K-10K RPS)
Focus: Distribute and optimize
- β Add horizontal scaling (multiple servers)
- β Set up a load balancer
- β Implement pagination for all list endpoints
- β Use fast JSON serializers
- β Expand caching strategy
Mindset: Your single server is maxed out. Time to distribute the load.
Phase 3: Scale (10K-100K RPS)
Focus: Edge optimization and async patterns
- β Deploy a CDN for static content and cacheable responses
- β Move slow operations to async workers
- β Implement async logging
- β Optimize database with better indexing and query patterns
Mindset: Every millisecond counts. Optimize the request path and offload work.
Phase 4: Massive Scale (100K-1M+ RPS)
Focus: Database distribution and protection
- β Implement database sharding
- β Add rate limiting to protect against abuse
- β Consider multi-region deployment
- β Implement circuit breakers and failover mechanisms
Mindset: Youβre operating at web scale. Focus on reliability, distribution, and resilience.
Critical Success Factors
1. Measure Before You Optimize
The bottleneck you imagine is rarely the real one. Use monitoring and profiling tools:
- Application Performance Monitoring (APM): New Relic, Datadog, or Grafana
- Database Profiling: Identify slow queries with EXPLAIN plans
- Load Testing: Use tools like k6, Gatling, or Apache JMeter
Donβt guessβmeasure, identify the bottleneck, then fix it.
2. Implement Quick Wins First
Some techniques have massive impact with minimal complexity:
- Connection pooling: 5-10x faster database operations
- Avoid N+1 queries: 40x faster response times
- Compression: 70-85% smaller payloads
- Basic caching: 100x faster for repeated queries
Start here before jumping to complex solutions.
3. Progressive Enhancement Over Big Rewrites
Add capabilities incrementally rather than rewriting everything:
- Add caching without changing your API
- Add horizontal scaling without modifying application code
- Add async processing for new features first
This reduces risk and delivers value continuously.
4. Know When NOT to Scale
- Traffic patterns: Donβt optimize for traffic spikes that happen once a year
- Cost vs. benefit: Sometimes accepting slower responses is cheaper than scaling
- Product stage: Pre-product-market fit? Focus on features, not scale
Premature optimization wastes time and money.
Common Scaling Mistakes to Avoid
β Mistake 1: Jumping to Microservices Too Early A well-optimized monolith can handle 10,000+ RPS. Microservices add complexityβonly adopt them when you have clear organizational or technical reasons.
β Mistake 2: Ignoring Database Optimization Adding more application servers wonβt help if your database is the bottleneck. Fix queries first.
β Mistake 3: Not Testing Under Load Load test before you have a problem, not during an outage. Know your limits.
β Mistake 4: Caching Everything Cache only whatβs expensive to compute and read frequently. Over-caching adds complexity.
β Mistake 5: Forgetting About Cache Invalidation As Phil Karlton said: βThere are only two hard things in Computer Science: cache invalidation and naming things.β Plan your cache invalidation strategy from day one.
Key Takeaways
-
Start Simple: A single optimized server with connection pooling, query optimization, and basic caching can handle 10,000+ RPS. Donβt over-engineer early.
-
Measure First, Optimize Second: Use APM tools and profiling to find your actual bottlenecks. The problem is rarely where you think it is.
- Low-Hanging Fruit Matters Most:
- Connection pooling (implement immediately)
- Avoid N+1 queries (implement immediately)
- Basic caching (implement early)
- Pagination (implement for all list endpoints)
- Compression (enable by default)
- Progressive Enhancement: Your architecture should evolve with your traffic:
- 1-1K RPS: Single server + optimizations
- 1K-10K RPS: Horizontal scaling + load balancing
- 10K-100K RPS: CDN + async patterns
- 100K-1M+ RPS: Sharding + multi-region
- Always Profile: The bottleneck you imagine is rarely the real one. Measure, donβt guess!
Final Thoughts
Scaling is a journey, not a destination. The techniques in this guide will take you from handling your first user to serving millions. The key is to:
- Start with fundamentals (connection pooling, query optimization)
- Add capabilities progressively (donβt jump to advanced techniques)
- Measure constantly (know your bottlenecks)
- Optimize deliberately (focus on impact, not complexity)
Remember: Twitter started as a Ruby on Rails monolith. Facebook started on a single server. Instagram scaled to 30+ million users with just 3 engineers. Good architecture and simple optimizations can take you incredibly far.
Build for todayβs needs with an eye toward tomorrowβs scale. When you need the next level, youβll knowβyour monitoring will tell you. Until then, keep it simple and keep shipping. π
Continue Your Journey
This guide covered the technical and infrastructure aspects of scaling APIs. But as your organization grows, youβll face a different kind of scaling challenge: organizational and design complexity.
β Continue to Part 2: Design & Architecture Strategies to learn about:
- API portfolio management
- Design-first methodologies
- Organizational patterns for large-scale API development
- Versioning strategies
- Governance and evangelism at scale