The slow page wasn't the database
Users kept telling us the Phone Numbers page was slow. Open it, wait, watch it hang for a second or two, then the list finally appears. The obvious suspect was the database: the table must be huge, the query must be missing an index, we must need to tune Postgres. Every one of those guesses was wrong.
The table was tiny and the query was instant#
The first thing I did was look at where the time actually went. The table behind the page had about 4,700 rows and took up 2.4 MB. The query that lists the numbers was a 2.3 ms sequential scan. Adding an index would have done nothing, because there was nothing to speed up. The database was not the problem.
So where did the second-and-a-half go?
The page was fetching a whole bot for every single number#
The slow part was the code that turned each phone number into a chunk of JSON for the frontend. For every number in the list, it called the function that builds the full detail payload for the bot attached to that number. That payload is the same one the bot’s edit screen uses: it pulls in the bot’s context, its post-call config, its languages, its transfer rules, its actions, its integrations, its stored files, and its per-minute cost, which then goes and looks up a currency.
That is 8 to 10 database queries. Per number. On the list page.
I measured it on a real account with 28 numbers: 400 SQL queries and 1.32 seconds to draw the page. 110 of those queries were just looking up the same currency over and over. This is the classic N+1: one query to get the list, then N more (a whole cascade, in this case) for each row. It never shows up when you test with two numbers on your laptop. It gets worse for every customer who has more numbers, and worse again under real traffic.
The fix was to ask what the page actually needed#
Here is the part worth remembering. I went and looked at what the frontend actually reads off that bot. Across every component on the page, the card, the dialogs, the pool manager, it only ever used three things: the bot’s id, its name, and its owner’s name. That is it. We were fetching the entire bot, ten queries deep, to show a name in a dropdown.
So instead of calling the big detail function, the list now returns a small dictionary with just those three fields. Because they are plain fields on a record, the ORM fetches them for every row in the list in one batched go instead of one row at a time. The cascade disappears.
Verified on the same production data: 400 queries and 1.34 seconds became 4 queries and 8 milliseconds. Same page, same data, roughly 150 times faster, and the diff was a few lines.
The lesson#
When a page is slow, it is easy to blame the data: the table must be too big, an index must be missing, the database needs tuning. Usually that is not it. Before touching the database, count how many queries the page actually runs. Most of the time the data is fine, and the code is just asking for far more than the screen will ever show. Fetch the few fields you need instead of the whole record, and let the database load them for every row at once instead of one row at a time.