FealDeal
Vayu Procure
A B2B procurement SaaS was told to rewrite in Go and split its database. We measured it instead, found 4,100 queries behind one screen, and took P95 from 11.2 seconds to 640 milliseconds.
640ms
P95 on the bid-comparison screen, down from 11.2 seconds
22%
Database CPU at peak, down from 95% — same instance, same data
9 min
Nightly spend-analytics run, down from 7 hours
41% lower
Lower monthly infrastructure spend after downsizing RDS
01
Vayu Procure sells a B2B procurement portal — 900 enterprise buyers running RFQs against a network of 26,000 suppliers. The product worked. It was just too slow to use.
Their largest customer, 19% of ARR, had written that the renewal depended on speed. The complaint was clear and fair. The supplier bid-comparison screen, the one screen a buyer uses to actually award business, was taking 11.2 seconds at P95. Their category managers had started copying data to Excel and comparing bids there. That is a sign a SaaS product is dying.
Two earlier consultants had suggested a rewrite in Go and splitting the database. Both would have taken months. Both would have been the wrong answer, and one of them would probably have finished after the company did.
Here is what an afternoon with a profiler and performance_schema really showed:
WHERE JSON_EXTRACT(attributes, '$.category') = ? against a 41-million-row table. No index can help that. Every filter click scanned the whole table.chunk() with an ORDER BY on an unindexed column. Every chunk sorted all 41M rows again. It was not processing data. It was re-sorting the table, 4,100 times, overnight.Their problem was never that MySQL is slow. They were asking it the wrong question, 4,100 times a page.
02
Two days of measuring: by request, with a profiler in staging using replayed live traffic, and by query, with performance_schema and the slow log on a replica. We did not touch a line of code until we could rank every fix by seconds saved per hour of work. Everything below is in that order.
The bid list showed six columns. It was loading 200 full Eloquent models, each dragging three levels of linked data behind it. We replaced the busy list path with a flat query-builder projection selecting exactly the six columns, joined once. Eloquent is a great tool for business rules and the wrong tool for a grid that is mostly read. 4,100 queries became 3.
The obvious fix for the JSON scan was "move line items to MongoDB/Elasticsearch". We did not, and we would argue this strongly. A second datastore means writing twice, delays in syncing, a new way to fail, a new thing to run, and a new thing to be on call for. Their queries used three known attributes. MySQL 8 stored generated columns with secondary indexes over those three attributes gave us 95% of the win for about 2% of the risk, in one migration, with no new servers. The full scan became an index range scan.
If they had needed open-ended text search, the answer would have been different. They did not, so it is not.
The dashboard reads the same twelve totals on every load. Working them out again by scanning 41M rows makes no sense. We moved the job to a read replica, replaced chunk() with keyset pagination on an indexed cursor (so there is no re-sorting, and no growing OFFSET cost), and materialised the twelve aggregates into a summary table the dashboard reads directly. Seven hours became nine minutes, and it no longer runs anywhere near the main database.
Sessions moved to Redis. ETag and Cache-Control with a version key to clear the cache on the catalogue endpoints, so a catalogue that changes weekly stops being rebuilt thousands of times an hour. Both took an afternoon. Together they took roughly a third of the load off the database.
The N+1 will come back in six months when a developer adds linked data to a list view, unless something stops them. We added a query-budget assertion to the CI suite: any request in the tests that sends more than 25 queries fails the build. It has fired four times since. That is four problems that did not happen.
We did not rewrite in Go. We did not shard. Both were suggested by people who had not measured the application, and both would have carried the same 4,100 queries into a more costly place to run them.
03
The bid-comparison screen was fixed and put live in nine days. The renewal was signed. The rest of the work was released over the next six weeks.
The query-budget check in CI is, honestly, the most valuable thing we left behind. Speed fixes fade over time. Without a guard, the same N+1 grows back, and someone calls another agency in eighteen months. A failing build is that guard. It has caught four problems before they went live, and it will keep catching them long after we are gone.
We spent nine days on the fix and two days on the measuring that told us which nine days to spend. The two days were the important ones. Every wrong answer this company had been given (rewrite it, split it, buy a bigger instance) came from someone who skipped them.
Under the hood
Technology
Services used
Two firms told us to rewrite the product. PRS spent two days with a profiler and then told us the database was fine and our code was asking it 4,100 questions per page. Nine days later we kept the account.