The Problem
Consider the homepage of an e-commerce application. The moment a user opens it, the system may need to retrieve product details, prices, inventory status, seller information, ratings, reviews, promotions, and personalized recommendations. Loading a single page can easily trigger dozens, or even more than 100, read operations. By comparison, that same user may complete only one purchase, creating just a small number of writes.
This gap between reads and writes appears in many products. A news article may be published once but opened hundreds of thousands of times. A hotel room may be added to a booking platform once but viewed by thousands of travelers. A popular movie may be uploaded to a streaming service only once, while millions of users request its metadata and playback information.
For many applications, read traffic begins at roughly ten times the write volume. In systems centered around browsing, feeds, media, or search, the ratio can quickly grow to 100:1 or more.
As read volume increases, the database eventually becomes the bottleneck. Queries take longer, connections become saturated, and the system struggles to respond within acceptable latency targets.
This is often not a problem that can be solved by simply rewriting application code. Every server has physical limits. CPUs can process only a fixed number of instructions, memory can store only a limited working set, and disks and networks provide finite throughput. Even a well-optimized database cannot serve unlimited requests.
Once the workload reaches those limits, further code-level tuning provides diminishing returns. The system must instead change how reads are stored, distributed, and served.
The Solution
Scaling read traffic usually happens in stages, beginning with straightforward database improvements and gradually moving toward a more distributed architecture.
The main approaches are:
- Improve query efficiency inside the existing database
- Distribute read traffic across multiple database servers
- Introduce caching outside the database
Improve Performance Inside the Database
Before introducing new infrastructure, first make sure the current database is being used efficiently. In many cases, read bottlenecks can be delayed or resolved through query optimization, better schema design, and appropriate indexing.
Indexing
An index is an auxiliary data structure that helps the database find records more efficiently. A useful comparison is a store directory: instead of walking through every aisle to locate a product, you check the directory first and go directly to the correct section.
Without an appropriate index, the database may need to perform a full table scan, examining every row until it finds the matching data. An index gives the database a much shorter path to the relevant records. In simplified terms, this can reduce lookup time from O(n) to O(log n). Rather than inspecting one million rows individually, the database may only need to follow around 20 steps through the index structure.

Databases support several kinds of indexes for different access patterns. B-tree indexes are the standard choice for most filters, range queries, and sorting operations. Hash indexes are better suited to exact-value lookups, while specialized index types can support full-text search, spatial data, and other domain-specific queries. A more detailed discussion of these structures belongs in a dedicated database indexing guide.
When improving read scalability, start by identifying the columns that appear frequently in filters, joins, and ORDER BY clauses. Those columns are often the strongest candidates for indexing. For example, a job platform may index the location field if users frequently search for nearby roles. An online store may index created_at or price when products are commonly sorted by recency or cost.
Upgrade the Hardware
In some cases, the simplest solution is to use a more powerful machine. It may not be architecturally exciting, but it can deliver immediate improvements. Replacing traditional hard drives with SSDs can make random disk access 10–100 times faster. Increasing memory allows a larger portion of frequently accessed data to remain in RAM, reducing slower disk reads. More powerful CPUs and additional cores also let the database execute a greater number of queries concurrently.
Upgrading a single server has clear limits and cannot address every scalability challenge. However, it is often the quickest way to increase capacity and give the system additional room to grow before introducing a more complex distributed architecture.

Denormalizing Data for Faster Reads
Another way to improve read performance is to reconsider how data is arranged inside the database.
Normalization organizes information into separate tables so that the same value does not need to be stored repeatedly. This reduces duplication and makes updates easier to manage, but it can also make read queries more expensive because related data must be reconstructed through joins.
Consider a hotel booking platform. In a normalized schema, customer details, reservations, properties, and rooms may all live in separate tables. Displaying a booking confirmation could require a query like this:
SELECT c.full_name, r.check_in_date, h.hotel_name, rm.room_type, r.total_price FROM customers c JOIN reservations r ON c.id = r.customer_id JOIN rooms rm ON r.room_id = rm.id JOIN hotels h ON rm.hotel_id = h.id WHERE r.id = 78291;
This design avoids duplicating customer and hotel information. However, when the platform must render thousands of booking pages every second, repeatedly joining several large tables can become costly. The database must search each table, match the relevant keys, and assemble the final result before returning it.
For a read-heavy workload, denormalization can improve performance by intentionally storing some duplicated information. Rather than rebuilding the booking details from several tables every time, the system can keep the most commonly requested fields together in a read-optimized table.
For example, a reservation_details table might store the customer name, hotel name, room type, dates, and final price alongside the reservation ID. Fetching the same page would then require only a simple lookup:
SELECT customer_name, hotel_name, room_type, check_in_date, total_price FROM reservation_details WHERE reservation_id = 78291;
The trade-off is that the same customer or hotel name may now appear in many rows. This consumes additional storage, but the resulting query is much simpler and may be significantly faster under heavy read traffic.
In systems where records are read much more frequently than they are modified, accepting additional storage and write complexity can be worthwhile.
Suppose a hotel changes its name. The system may need to update that value in the main hotel table as well as in any denormalized records that contain a copy. That update becomes more complicated, but hotel names change far less often than reservation pages are viewed.
Denormalization therefore shifts work away from reads and toward writes. Duplicate fields make data retrieval faster, but they also introduce consistency challenges because every copy must remain synchronized.
Before applying this technique, consider the workload’s read-to-write ratio. If the data changes constantly, maintaining many duplicated copies may create more problems than it solves. If reads dominate and updates are relatively rare, denormalization can be an effective optimization.
Materialized views apply a similar idea to expensive calculations. Instead of recomputing the same aggregation whenever a page is requested, the database calculates it ahead of time and stores the result.
For example, calculating the average rating of every hotel on each page load would require repeatedly scanning and grouping a large reviews table:
SELECT h.id, AVG(rv.score) AS average_score FROM hotels h JOIN reviews rv ON h.id = rv.hotel_id GROUP BY h.id;
A materialized view can perform this work in advance:
CREATE MATERIALIZED VIEW hotel_rating_summary AS SELECT h.id, AVG(rv.score) AS average_score FROM hotels h JOIN reviews rv ON h.id = rv.hotel_id GROUP BY h.id;
The application can then read the precomputed rating directly instead of repeating the full aggregation for every request. This is especially useful for dashboards, reporting systems, and other workloads that repeatedly run complex calculations over large datasets.

Scale the Database Across Multiple Servers
Once a single database machine can no longer keep up with demand, the next step is to distribute the workload across additional servers. This is where the architecture becomes more scalable, but also more complicated.
As a rough guideline, a properly indexed database may begin to require horizontal scaling—or an external cache—when read traffic grows beyond approximately 50,000 to 100,000 requests per second.
This threshold should not be treated as a fixed limit. The real capacity depends on factors such as query complexity, access patterns, schema design, hardware, result size, and connection behavior. In a system design interview, however, a reasonable estimate is usually enough to explain why the system must move beyond a single database instance.