Fallacies of Distributed Computing
Bandwidth Is Infinite (It's Not)
Read aloud is unavailable in this browser.
When the Pipe Gets Full
We've learned that networks fail (Fallacy 1) and that data takes time to travel (Fallacy 2). Now we confront the third fallacy: the assumption that bandwidth is infinite—that you can send as much data as you want without consequence.
In the age of gigabit home internet and multi-terabit backbone links, it's easy to believe bandwidth is no longer a concern. Modern networks carry staggering amounts of data. But bandwidth limits appear in surprising places, and when you hit them, the consequences are severe: dropped packets, exponentially increasing latency, and complete system degradation.
That '10 Gbps' connection to your service? It's shared by thousands of processes and containers on the same machine. The 'unlimited bandwidth' between availability zones? It's shared by thousands of customers. Your application never has bandwidth to itself.
This page explores where bandwidth limits manifest, how they interact with latency and reliability, and the architectural patterns needed to design systems that respect bandwidth as the precious, finite resource it is.
Understanding Bandwidth
Bandwidth is the maximum rate at which data can be transferred over a network connection. It's measured in bits per second (bps) and represents the capacity of the "pipe." But several key distinctions are often misunderstood:
Bandwidth vs. Throughput:
- Bandwidth: Maximum theoretical capacity
- Throughput: Actual data transfer rate achieved in practice
Throughput is always less than bandwidth due to protocol overhead, congestion, and other factors.
Bandwidth vs. Latency:
- Bandwidth: How much data can flow (width of the pipe)
- Latency: How long data takes to travel (length of the pipe)
High bandwidth doesn't mean low latency. A satellite link might have high bandwidth but 600ms latency. A direct fiber might have lower bandwidth but 1ms latency.
| Connection Type | Typical Bandwidth | Shared By | Real Availability |
|---|---|---|---|
| Process to L1 Cache | ~200 GB/s | Single CPU core | Full |
| RAM Access | ~50 GB/s | All processes on machine | Contentious under load |
| Local SSD (NVMe) | 3-7 GB/s | All processes on machine | Contentious under load |
| Intra-rack (25 GbE) | 25 Gbps (~3 GB/s) | Dozens of machines | Heavily shared |
| Cross-datacenter | 10-100 Gbps aggregate | Thousands of services | Your share is tiny |
| Cross-region (cloud) | Varies, often capped | All customers in region | Metered and expensive |
| Public Internet | 1-10 Gbps burst | All internet traffic | Highly variable |
| Mobile 4G/LTE | 10-50 Mbps typical | Cell tower congestion | Often much lower |
A critical concept: the bandwidth-delay product (BDP) is bandwidth × round-trip time. It represents how much data can be 'in flight' at once. A 1 Gbps link with 100ms RTT has a BDP of 12.5 MB. TCP congestion windows must reach this size to fully utilize the link. This is why high-bandwidth, high-latency links are hard to use efficiently.
Where Bandwidth Limits Bite
Bandwidth constraints manifest in different ways depending on the network segment and use case. Understanding where limits exist helps you design around them.
- •Mobile and Last-Mile Connections — Users on 3G, congested WiFi, or rural connections have bandwidth measured in Kbps, not Gbps. Your 10MB payload that loads in 1 second at headquarters takes 80 seconds for them.
- •Cross-Region Data Transfer — Cloud providers charge significantly for inter-region transfer (often $0.02-0.09/GB), creating cost limits if not capacity limits. A system that copies 10TB/day cross-region costs $200-900/day just in transfer fees.
- •Shared Network Interfaces — In containerized environments, many containers share a single NIC. Under load, noisy neighbors can consume your bandwidth allocation.
- •Database Replication — Synchronous replication bandwidth limits the rate of writes. If your replica can't keep up, lag grows indefinitely.
- •Backup and Bulk Transfer — Large data movements (backups, analytics ingestion, data migration) compete with production traffic for bandwidth.
- •API Payloads — Returning megabytes of JSON when clients need kilobytes wastes bandwidth and increases latency.
- •Media Streaming — Video and audio consume massive bandwidth. A 1080p video stream at 5 Mbps serving 10,000 concurrent users requires 50 Gbps—more than most companies' total internet connectivity.
The interaction between bandwidth and latency:
When bandwidth limits are reached, packets must queue. This queueing adds latency—potentially unbounded latency if the queue fills faster than it drains. This is called bufferbloat, and it's why a saturated network connection doesn't just slow down but becomes completely unusable.
Example scenario:
Consider a 100 Mbps link carrying 120 Mbps of traffic:
- Excess 20 Mbps accumulates in router buffers
- If buffers are 64 KB, they fill in ~25 milliseconds
- After buffers fill, packets are dropped
- TCP senders detect loss, back off, and retry
- User experiences packet loss + latency spikes + retransmission delays
This cascading failure mode—bandwidth saturation leading to latency explosion leading to timeouts leading to retries leading to more bandwidth consumption—is devastatingly common.
When bandwidth is exceeded, retries make it worse. More retries mean more traffic, which means more congestion, which means more retries. Without backpressure mechanisms, systems can enter death spirals where the only recovery is to shed load entirely.
The Cost of Bandwidth
Even when bandwidth is technically available, it's rarely free. Data transfer costs can dominate cloud bills, especially for systems that move data across regions or to end users.
| Transfer Type | AWS | GCP | Azure | Notes |
|---|---|---|---|---|
| Same AZ | Free | Free | Free | Encouraged architecture |
| Cross-AZ (same region) | $0.01/GB | $0.01/GB | $0.01/GB | Adds up fast at scale |
| Cross-Region (same continent) | $0.02/GB | $0.02/GB | $0.02/GB | Significant for DR/multi-region |
| Cross-Region (intercontinental) | $0.05-0.09/GB | $0.05-0.12/GB | $0.05-0.12/GB | Major cost driver |
| Internet Egress | $0.05-0.09/GB | $0.05-0.12/GB | $0.05-0.09/GB | Often the largest line item |
| CDN (per GB delivered) | $0.02-0.08/GB | $0.02-0.08/GB | $0.02-0.08/GB | Volume discounts available |
Case study: Video streaming costs
Consider a video platform serving 4K content:
- 4K stream: ~25 Mbps = ~3.125 MB/second
- 2-hour movie: ~22.5 GB
- At $0.05/GB egress, each movie stream costs: $1.12 in bandwidth alone
With 1 million views, that's $1.12 million in bandwidth for a single movie. This is why Netflix, Disney+, and other streaming services invest billions in CDN infrastructure, peering arrangements, and edge caching.
Case study: Multi-region database replication
- Database replication generating 100 GB/day of WAL data
- Cross-region replication to 3 regions: 300 GB/day
- At $0.05/GB: $15/day = $450/month = $5,400/year
And that's just replication traffic—not including queries, analytics, or backups.
Compression can reduce bandwidth costs by 3-10x for text-based formats. Protocol optimization (HTTP/2, gRPC) reduces overhead. Edge caching eliminates repeated transfers. These investments often pay for themselves many times over in reduced egress costs.
Payload Size Anti-Patterns
Developers who assume infinite bandwidth often produce APIs and data formats that work in development but fail in production, especially on constrained networks.
- •Overfetching — API returns 50 fields when the client needs 3. GraphQL exists specifically to address REST overfetching.
- •Unbounded List Returns — API returns all 100,000 items instead of paginating. Each response is megabytes of JSON.
- •Uncompressed Responses — JSON or XML sent without gzip/brotli compression. Compression typically reduces size by 70-90%.
- •Base64 Binary Data — Encoding binary in Base64 adds 33% overhead. Use multipart forms or separate binary endpoints.
- •Verbose Serialization — XML instead of JSON, JSON instead of Protocol Buffers. Format choice matters at scale.
- •Redundant Data — Sending the same user object nested in every item of a list. Normalize references instead.
- •Unoptimized Assets — 4K images when thumbnails suffice. Full-resolution assets on mobile. No responsive images.
- •Logging Everything — Shipping verbose logs over the network to a central service. Overwhelming log aggregators and burning bandwidth.
1234567891011121314151617181920212223242526272829
// ❌ Overfetching: 2KB per user{ "users": [ { "id": "usr_123", "email": "alice@example.com", "name": "Alice Johnson", "avatar": "https://cdn.../lg.jpg", "bio": "Software engineer...", "location": "San Francisco, CA", "website": "https://alice.dev", "createdAt": "2020-01-15T00:00:00Z", "updatedAt": "2024-01-15T12:30:00Z", "settings": { "theme": "dark", "language": "en", "notifications": { ... }, "privacy": { ... } }, "statistics": { "followers": 1234, "following": 567, "posts": 89 } // ... 30 more fields } // × 1000 users = 2MB response! ]}12345678910111213141516171819202122
// ✅ Minimal: 100 bytes per user{ "users": [ { "id": "usr_123", "name": "Alice Johnson", "avatar": "https://cdn.../sm.jpg" } // × 1000 users = 100KB response! ]} // 20x smaller payload// Faster to transfer// Faster to parse// Less memory usage// Better mobile experience // Need more fields?// Client can fetch on demand:// GET /users/usr_123/profile// GET /users/usr_123/settingsGraphQL was created specifically to solve overfetching and underfetching. Clients specify exactly which fields they need, and the server returns only those fields. This can reduce payload sizes by 50-90% for complex data models.
Designing for Bandwidth Constraints
Accepting that bandwidth is finite leads to specific design patterns that minimize data transfer and maximize the value of every byte sent.
- •Pagination — Never return unbounded collections. Use cursor-based pagination for large datasets. Return 20-50 items per page, not 10,000.
- •Compression — Enable gzip/brotli for all text responses. Modern algorithms compress JSON by 70-90% with minimal CPU cost.
- •Sparse Fieldsets — Let clients specify which fields to include. JSON:API, GraphQL, and OData all support this.
- •Efficient Serialization — Use Protocol Buffers, MessagePack, or Avro for internal service communication. 2-10x smaller than JSON.
- •Incremental Updates — Instead of sending full state, send deltas. Operational transforms, CRDTs, and patch formats reduce transfer size.
- •Caching at Every Layer — CDN, reverse proxy, application cache, client cache. Every cache hit is bandwidth saved.
- •Conditional Requests — Use ETags and If-None-Match. Return 304 Not Modified instead of resending identical data.
- •Image Optimization — Serve appropriately sized images. Use modern formats (WebP, AVIF). Implement lazy loading.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
/** * Example: Bandwidth-optimized API endpoint * Demonstrates multiple optimization techniques */ import { Request, Response } from 'express';import { compress } from 'compression'; interface ListOptions { cursor?: string; limit?: number; fields?: string[]; // Sparse fieldsets format?: 'json' | 'protobuf';} interface PaginatedResponse<T> { items: T[]; nextCursor?: string; hasMore: boolean;} class ProductController { // Maximum items per request to bound response size private readonly MAX_LIMIT = 50; // Fields allowed to be requested private readonly ALLOWED_FIELDS = new Set([ 'id', 'name', 'price', 'thumbnail', 'category', 'rating', 'stock', 'description', 'images' ]); // Default fields for minimal response private readonly DEFAULT_FIELDS = ['id', 'name', 'price', 'thumbnail']; async listProducts(req: Request, res: Response): Promise<void> { const options = this.parseOptions(req); // Check ETag for conditional request const etag = await this.calculateETag(options); if (req.headers['if-none-match'] === etag) { res.status(304).end(); // Not Modified - saves bandwidth! return; } // Fetch data with pagination const data = await this.fetchProducts(options); // Project to requested fields only (sparse fieldsets) const projected = this.projectFields(data.items, options.fields); // Set caching headers res.set({ 'ETag': etag, 'Cache-Control': 'public, max-age=60', 'Vary': 'Accept-Encoding' // For compression variants }); // Response automatically compressed by middleware if (options.format === 'protobuf') { res.type('application/x-protobuf'); res.send(this.toProtobuf({ items: projected, ...data })); } else { res.json({ items: projected, ...data }); } } private parseOptions(req: Request): ListOptions { const requestedFields = req.query.fields as string | undefined; const fields = requestedFields ? requestedFields.split(',').filter(f => this.ALLOWED_FIELDS.has(f)) : this.DEFAULT_FIELDS; return { cursor: req.query.cursor as string | undefined, limit: Math.min( parseInt(req.query.limit as string) || 20, this.MAX_LIMIT ), fields, format: req.accepts(['json', 'application/x-protobuf']) === 'application/x-protobuf' ? 'protobuf' : 'json' }; } private projectFields<T extends Record<string, unknown>>( items: T[], fields: string[] ): Partial<T>[] { return items.map(item => { const projected: Partial<T> = {}; for (const field of fields) { if (field in item) { projected[field as keyof T] = item[field as keyof T]; } } return projected; }); } // Placeholder implementations private async fetchProducts(options: ListOptions): Promise<PaginatedResponse<Record<string, unknown>>> { return { items: [], hasMore: false }; } private async calculateETag(options: ListOptions): Promise<string> { return '"abc123"'; } private toProtobuf(data: unknown): Buffer { return Buffer.from([]); }}Each optimization compounds. Sparse fieldsets reduce data by 50%. Compression reduces by 75%. Conditional requests eliminate 90% of unchanged responses. Combined, you might transfer 1% of the naive payload—100x improvement.
Backpressure and Flow Control
When systems produce data faster than consumers can handle, you need backpressure—mechanisms that signal producers to slow down and prevent overwhelming the network or downstream services.
- •Blocking Backpressure — Producer waits when consumer is full. TCP does this automatically with receive windows. Simplest but can cause deadlocks if not careful.
- •Dropping — When buffers fill, drop new data. Acceptable for metrics, logs, or data that can be sampled. Unacceptable for transactions.
- •Rate Limiting — Explicitly cap production rate. Prevents overload but may waste available capacity.
- •Dynamic Throttling — Adjust production rate based on consumer feedback. Complex to implement correctly.
- •Buffer with Bounded Queues — Accept bursts but bound queue size. When full, apply one of the above strategies.
- •Load Shedding — When overwhelmed, reject new work entirely (return 503) rather than degrading all requests.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
/** * Example: Bounded buffer with backpressure * Producer blocks when buffer is full */ class BoundedBuffer<T> { private buffer: T[] = []; private waitingProducers: Array<() => void> = []; private waitingConsumers: Array<(item: T) => void> = []; constructor(private readonly maxSize: number) {} async put(item: T): Promise<void> { // If buffer is full, wait for space if (this.buffer.length >= this.maxSize) { await new Promise<void>(resolve => { this.waitingProducers.push(resolve); }); } // Check if consumer is waiting const waitingConsumer = this.waitingConsumers.shift(); if (waitingConsumer) { // Hand off directly to consumer waitingConsumer(item); } else { // Add to buffer this.buffer.push(item); } } async take(): Promise<T> { // If buffer is empty, wait for item if (this.buffer.length === 0) { return new Promise<T>(resolve => { this.waitingConsumers.push(resolve); }); } // Get item from buffer const item = this.buffer.shift()!; // Wake up a waiting producer if any const waitingProducer = this.waitingProducers.shift(); if (waitingProducer) { waitingProducer(); } return item; } get size(): number { return this.buffer.length; }} // Usage: Producer-Consumer with backpressureasync function producerConsumerExample() { const buffer = new BoundedBuffer<number>(100); const processedItems: number[] = []; // Fast producer (will be slowed by backpressure) const producer = async () => { for (let i = 0; i < 10000; i++) { await buffer.put(i); // Blocks when buffer full console.log(`Produced: ${i}, Buffer size: ${buffer.size}`); } }; // Slow consumer (processes at its own pace) const consumer = async () => { for (let i = 0; i < 10000; i++) { const item = await buffer.take(); await simulateProcessing(100); // 100ms per item processedItems.push(item); console.log(`Consumed: ${item}`); } }; await Promise.all([producer(), consumer()]);} function simulateProcessing(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms));}Backpressure must flow through the entire system. If your API applies backpressure but your message queue doesn't, data accumulates in the queue until it fails. Design backpressure as a system property, not a component property.
Monitoring Bandwidth Usage
You can't optimize what you don't measure. Bandwidth monitoring should be a first-class observability concern, especially for systems with significant data transfer.
- •Bytes In/Out per Service — Track ingress and egress for each service. Identify which services are bandwidth hogs.
- •Payload Size Distribution — Histograms of request and response sizes. Identify oversized payloads.
- •Bandwidth Utilization % — How much of available bandwidth is being used? Alert before saturation.
- •Queue Depths — Deep queues indicate producers outpacing consumers—a bandwidth or processing issue.
- •Compression Ratios — Are you actually getting expected compression benefits?
- •Cost Attribution — Cloud billing for data transfer. Break down by service, customer, region.
- •Error Rates During High Bandwidth — Correlate dropped connections and timeouts with bandwidth spikes.
| Tool | Purpose | Best For |
|---|---|---|
| Prometheus + Grafana | Infrastructure metrics visualization | General bandwidth monitoring |
| Envoy + Jaeger | Service mesh with detailed traffic tracing | Microservices bandwidth |
| CloudWatch/Stackdriver | Cloud provider native metrics | AWS/GCP egress tracking |
| VPC Flow Logs | Network-level packet captures | Deep traffic analysis |
| Wireshark | Packet-level analysis | Debugging specific issues |
| iftop/nload | Real-time bandwidth visualization | Quick diagnostics |
Bandwidth issues often develop gradually. A 10% week-over-week increase in egress might not trigger threshold alerts but indicates a growing problem. Implement anomaly detection and trend alerts, not just simple thresholds.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.