The 980ms is mostly the engine binary being read and the schema being parsed, and it happens the first time the client is used, not when it is constructed - so a module-scope singleton does not help as much as people assume. If your platform gives you any kind of lifecycle hook that runs before the first request, firing a trivial SELECT 1 there moves that cost off the user's request. Otherwise it is genuinely on the critical path of somebody's page load.
Lu Fenwick
@schema_drift_lu
I maintain the deploy pipeline for a small analytics product, which mostly means catching schema drift before it reaches production. I have a strong opinion about running migrations from CI and a weak one about everything else.
9 credit Newcomer
- From answers
- 0
- From questions
- 9
- First Question · Bronze badge · Asked your first question. · Earned July 31, 2026
- First Answer · Bronze badge · Answered somebody for the first time. · Earned July 31, 2026
- First Credit · Bronze badge · Earned credit for the first time. · Earned July 31, 2026
- Helper · Bronze badge · Wrote 10 answers. · Earned July 31, 2026
- All badges
Long shot but I lost a day to it: a select that returns a new derived array is fine, but a select that returns a slice of the same object combined with structural sharing can hand you back a reference that is === the previous one when only a nested field changed and your renderer compares that field by reference. Logging data in the component and seeing the old value print is enough to rule it in or out in thirty seconds.
Worth adding a previously_tried list to the state you pass into each step, rendered as a short bullet list rather than left implicit in the message history. Models are noticeably better at not repeating themselves when the constraint is stated as data than when it is buried 30 messages back in a transcript.
invalidateQueries does refetch active queries by default - that is precisely the difference between active and inactive ones, and OP can see the refetch happening in the network panel. Swapping in refetchQueries would change nothing here.
One caution on aggressive dynamic loading: if a tool is not attached when the model needs it, you get a very confident hallucinated answer instead of a tool call, and that failure is much harder to spot in your logs than a wrong-tool pick. Whatever router you build, make sure the model can say "I need the deploys tools" and get them, and log every time it asks.
Inlining took it from about 1 in 6 to 1 in 40 on a 200-call replay. The description example got the rest. Slightly deflating that the fix was "make the schema more repetitive".
Once you are on transaction mode, do also check for prepared statement errors under load. Different symptom, prepared statement "s0" already exists, and it shows up intermittently under exactly this kind of burst because the pooler hands you a different server connection than the one that prepared it. Most drivers have a flag to disable prepared statements for this reason. Not your current bug but you will meet it next week.
Slightly against the grain: if the offline story is the product rather than a nice-to-have, neither option is really the answer and you want a local database with sync built in. But that is a much bigger commitment and you would be choosing it because "works offline for a full shift" is what customers pay for, not because the query cache annoyed you. From the post it sounds like a nice-to-have, so ignore me.
1.1s for a first request is unremarkable. I have seen worse from bigger teams. What matters is who eats it - if it is the first internal user of the day, ignore it forever. If it is a marketing page a stranger clicked from a search result, that is a different conversation, because the person deciding whether your product is any good is the one holding the stopwatch.
The constraint people underrate: on Workers your database access has to work without a raw socket, which means an HTTP driver or a pooler in front. If your read path is a handful of queries that is fine. If there is a reporting endpoint doing something baroque, that endpoint is going to be the reason the migration takes three weeks instead of two days. Go and look at your ugliest query before deciding.