2.3 MB per click: how my console got slower every week
My internal console wasn't slow because of code. It was slow because of what each row carried, and where the server lived.
I run a small console that sits on top of my agents. It's where I see what ran, what it cost, and what needs my sign-off. It's internal, used by a handful of people, and it felt slow. Every click waited a second or two with nothing on screen.
Slow internal tools are easy to forgive. That's the trap. I decided to measure instead of guess.
Measure the queries, not the page
I timed each query the home view makes, from my laptop against the live database:
| Query | Time | Size |
|---|---|---|
| A tiny table (15 rows) | 243 ms | 2 KB |
| The recent runs, 300 max | 2,175 ms | 2.3 MB |
| The same runs, without two columns | 251 ms | 21 KB |
Same rows, same query, one change: two JSON columns. That was 2.3 MB for 60 runs.
What was in those columns
Every agent run stores a record of each step: its inputs and outputs, so I can audit any run later.
That's a feature. But the list view was selecting *, so it downloaded every run's full step output:
the extraction detail, the gate decisions, all of it. That's 20 to 47 KB per run. The view only needed a
state, a headline and two counts.
And it got worse every week. Each new run added another 40 KB to every click. At 300 runs it would have been 12 MB per page view.
Fix 1: select what you show
Postgres JSON paths make this easy. Instead of the whole outputs column, the list asks for the few
fields it renders:
sqlselect id, status, started_at,
outputs->'readout' as readout,
outputs->'compare'->'named_this' as named_this,
outputs->'this_week'->>'end' as week_end
from runs ...
Then one small function rebuilds those fields into the same shape the views already expected, so no view had to change. The page you open in full still loads everything. Lists load light.
Fix 2: one wave, not five
The page made its reads in sequence: who am I, which clients, the runs, then trends, then the next scheduled run. Each one waited for the one before. None of them depended on the others, so they now go out together.
Every round trip you remove is worth more than you think, which brings me to the part I'd missed.
Fix 3: where does your server live?
My database is in Singapore. My app's server functions ran in the platform's default region, on the US east coast. Every query crossed the planet and came back: about 250 ms per round trip, and the page made several.
This is one setting. The function region moved to Singapore, next to the database. Server-to-database round trips go from hundreds of milliseconds to a few. The only long trip left is the one to the user's browser, which happens once per page instead of once per query.
Fix 4: count in the database
Two parts of the page downloaded raw rows just to add them up: answers per day for a sparkline, and spend per client this month. Each grows linearly forever. Three tiny read-only SQL functions now return the totals instead: one row per day, one row per client. They're callable only by the server's service key.
I wrote them so the app falls back to the old reads if the functions are missing. That way deploying the code and applying the SQL can happen in either order. Small thing, big relief.
Fix 5: answer the click immediately
Even a fast page should answer the click before the data arrives. Every view now has a loading skeleton in the shape of what's coming, so nothing jumps. The nav item lights up the moment it's clicked, not when the next page finishes.
The numbers
Measured the same way, before and after (warm, from my laptop, so the Singapore round trip is still in there):
| View | Before | After |
|---|---|---|
| Home | 2.30 s | 0.60 s |
| A research overview | 1.83 s | 0.88 s |
| A run | 0.87 s | 0.72 s |
And it no longer gets slower every week.
What I'd tell myself a month ago
select *on a table with JSON blobs is a time bomb. It's fine on day one and broken by month three.- Measure queries, not pages. The page time hides which query is guilty.
- Put compute next to data. It's often one setting, and it multiplies every other fix.
- Count where the data lives. Don't download rows to add them up.
- Make the click answer at once. Perceived speed is part of speed.