There's a particular kind of slowdown that catches teams off guard, because nothing appears to have broken. No failed deploy, no error, no obvious cause. The app just gets a little heavier every month until one day someone says out loud that it "feels slow now," and everyone quietly agrees. It's usually worst at the busy hour — Monday morning, end of month, the middle of a sale — exactly when you most need it to hold up.
This is different from a database that was always slow. If yours has never been fast, my write-up on why a SQL Server database is slow covers the usual suspects. This piece is about the other story — the one where it used to be fast. That version is almost always a scale problem: queries and a schema that were perfectly fine at small size, quietly falling apart as data and concurrency grew. The good news is the same as before — it's rarely the hardware, and it's fixable without a rewrite.
1. The query didn't change — but its cost did
Most queries have a hidden growth rate. A well-indexed lookup barely cares whether the table has a thousand rows or ten million — it jumps straight to what it needs. But a query that scans the table does more work every time the table gets bigger. At a thousand rows that scan is invisible. At ten million it's a full second, then two, and it grows right alongside your data.
That's why the slowdown feels gradual and causeless: the code is identical, so nobody looks at it. What changed is the row count underneath it. The queries that hurt you at scale are almost always the ones whose cost rises in a straight line with the size of the table — and they were the cheapest, most ignorable queries back when you wrote them.
2. Your indexes were sized for last year's data
An index that made perfect sense at launch can stop being enough as the table grows and the queries against it get more specific. Early on, an index on a single column is plenty. Later, you're filtering by three things at once — a status, a date range, and a customer — and a single-column index only gets the database part of the way there before it has to sift through the rest by hand.
As data grows, indexing stops being "did I add an index?" and becomes "does this index match how the query actually filters and sorts?" A composite index in the right column order can turn a query that got slower every month back into an instant one. This is the highest-leverage work at scale, and it's still low-risk — you're not changing application behaviour, just giving the database a better shortcut for the questions it now gets asked.
3. Pagination you skipped when the table was small
Loading "all the records" is a decision that ages badly. A dropdown of every customer, a report that pulls the whole history, a list screen with no page limit — all fine when there are two hundred rows, all quietly lethal at two hundred thousand. The database reads and ships every row, the network carries it, and the browser tries to render it, every single time the page opens.
The fix is to stop asking for everything: page large lists, load results in chunks, and let the user filter down to what they actually need before you fetch it. This is one of the most common causes of "the app got slow as we grew," and one of the most satisfying to fix, because the same screen goes from a multi-second hang to instant without touching a single business rule.
4. Under load, queries start fighting each other
Here's the part that only shows up when you grow the number of people, not just the number of rows. A query that runs fine on its own can behave very differently when fifty of them run at once. They compete for the same rows, the same pages, the same locks — and when one operation holds on too long, everything behind it waits in line. Users experience this as random freezes that nobody can reproduce on a quiet test machine, because on a quiet machine there's nothing to queue behind.
This is why a database can look healthy in isolation and still fall over at peak. The usual causes are transactions that do too much at once and hold their locks too long, and writes that collide with reads on hot rows. Shortening transactions, adjusting how the app reads data, and — again — faster queries all help, because a query that finishes sooner holds its locks for less time and gets out of everyone else's way.
5. The connection pool has a ceiling you're now hitting
Applications don't open a fresh database connection for every request — they borrow one from a pool and hand it back. That pool has a limit. While you're small, you never come close to it. As traffic grows, you start running out of connections at the busy hour: requests sit and wait for one to free up, and the app feels frozen even though the database itself is barely working.
The instinct is to make the pool bigger, but that usually just moves the queue from the app to the database. The real fixes are the ones above — faster queries hold connections for less time, so each connection serves more requests — plus making sure connections are actually being returned to the pool promptly and not held open longer than they need to be. Connection exhaustion is a classic "worked fine until we grew" failure, and it's invisible until you look for it specifically.
6. Old data nobody ever archived
Some tables only ever grow. Audit logs, events, notifications, historical transactions — every one of them adds rows forever and deletes nothing. For a while that's fine. Then the table is the biggest thing in the database, every query against it is dragging years of history it doesn't need, and the indexes on it have grown huge.
Most systems only ever query recent data in these tables, so the fix is to stop keeping everything hot: archive or roll off old rows, or partition the table so the database can skip the history it isn't asking about. Deciding what "old" means is a business question, but once it's answered, keeping the working set small is one of the most durable ways to stop a database getting slower every single month.
7. Doing the same expensive work over and over
As traffic climbs, you start recomputing the same answers constantly. The same dashboard totals, the same reference lists, the same "expensive but rarely-changing" query, run again for every user who loads the page. At low traffic that waste is invisible. At high traffic it's a big share of what the database is doing all day.
A sensible caching layer — holding the results of expensive, slow-changing queries for a short time instead of recomputing them per request — takes a huge amount of repeated load off the database. Caching adds its own considerations, mainly around when to refresh, so it's a tool to apply deliberately rather than everywhere. But for the handful of genuinely hot, genuinely expensive reads, it's often the difference between a database that strains at peak and one that stays comfortable.
A database that slows down as you grow isn't failing — it's telling you which decisions were made for a smaller version of the system. The fix is to make them again for the size you are now.
How to build a database that stays fast under load
The reassuring part of all this is that "stays fast as it grows" is a property you can design for, not luck you hope for. It comes down to a few habits, and they're the same ones I apply when I build and tune SQL Server databases for systems that have to hold up in production.
Index for the questions you actually ask
The right indexes, in the right column order, matched to how your real queries filter and sort — checked against real data volumes, not the handful of rows on a developer's laptop. This is the single biggest lever for staying fast as data grows, and it costs nothing but knowing what to add.
Never load more than you'll use
Page every large list, fetch only the columns a screen displays, and let users narrow down before you query. Designing this in from the start means the same screens that work at a thousand rows still work at a million, with no emergency later.
Keep the working set small
Decide early what happens to data as it ages, so your busiest tables don't carry years of history into every query. Archiving and partitioning aren't glamorous, but they're what stops steady, invisible decay.
Test at the size you're growing into
Most scale problems are trivial to catch before they hit users — if you look. Running the important queries against a realistic amount of data, and putting the system under realistic concurrent load, surfaces the query that scales badly or the lock that contends while it's still cheap to fix, instead of during a Monday-morning outage.
Was your app fast, and now it isn't?
I do SQL Server performance and scaling work for .NET applications — finding the queries, indexes and patterns that stopped keeping up as you grew, fixing them, and proving the difference with numbers. If your database is buckling under load or getting slower every month, tell me what you're seeing and I'll take a look. For the general case, my write-up on why a SQL Server database is slow is a good companion read.
Start a project →