The Rollup That Stopped Adding Up
A vendor-built flow rolled up Timecard Splits for rev rec — until 30,000 records per entity met the Apex CPU limit.
It didn't start with a red banner or a failed deployment. It started with a finance user asking a very reasonable question: why don't the hours on this project look right?
The rollup on the Project record — the number revenue recognition depends on — had quietly stopped agreeing with the timecards underneath it. No error email. No broken screen. Just a total that was wrong, sitting in exactly the place finance looks when it's time to recognize revenue.
The setup
The mechanism was a vendor-built flow. When someone set the revenue recognition date on the company record, the flow woke up, fetched that entity's Timecard Splits, and rolled the hours up for rev rec — looping through the records and totalling as it went.
And to be fair to the vendor: it worked. It had worked for a long time. At the volumes it was built against, it was a perfectly reasonable design.
The error nobody saw
Then the volume grew — past 30,000 Timecard Splits for a single entity — and the flow began dying mid-run with one of the platform's least forgiving errors: Apex CPU time limit exceeded.
Two things make this failure nastier than most. First, the name misleads: you don't need a line of Apex to hit it. Flow elements burn the same shared CPU budget as Apex code — roughly ten seconds for the whole synchronous transaction. A loop touching 30,000 records one element at a time eats that budget with ease.
Second — and this is the part worth remembering — the failure wasn't loud where it mattered. The flow died, the rollup silently stopped being maintained, and the numbers on the Project record just… stopped moving. The person who caught it wasn't an admin watching debug logs. It was a finance user staring at a total that didn't add up.
The scariest failure mode isn't a red error. It's a number that's quietly wrong.
Why the flow was doomed
Flows process records the way a person would: one at a time. Get the records, loop, add to a running total. That's row-by-row thinking, and it carries a hidden expiry date — a volume beyond which the arithmetic no longer fits inside the transaction's budget.
This is the thing about large data volumes: LDV issues are rarely data problems. They're assumption problems. The flow wasn't wrong — its unstated assumption was: an entity will never have that many splits. Nothing in the org changed the day it broke. The data simply grew, exactly the way live systems do.
The fix — batch Apex, with one subtlety
The rebuild moved the rollup into batch Apex. Batches chunk the work, and each chunk executes with a fresh set of governor limits and an asynchronous CPU budget — so 30,000 splits (or 300,000) stop being scary.
The subtlety is boundaries. One project's splits can span multiple batch chunks, and a rollup built from half a project's records is worse than no rollup at all. So the job accumulates hours per project across the whole run and writes each project's total only when it's complete — guaranteeing every Project record ends up with all of its timecards rolled up, not just the ones that happened to share a batch.
Questions I now ask in every design review
- At what volume does this break? Every design has a ceiling. Name it before production finds it for you.
- Is this iterating or aggregating? Row-by-row loops in a flow are fine for dozens of records. They are not an aggregation strategy.
- What does failure look like? An error a user sees — or a financial number that quietly drifts? Silent wrongness costs far more than loud breakage.
- Who notices first? If the honest answer is "finance, eventually," the design needs monitoring as much as it needs a fix.
The vendor's flow wasn't a bad build. It was a build with an unexamined assumption, and data volume is the assumption that always gets examined eventually — by production, on a schedule of its own choosing. Ask the volume question early, while it's still cheap to answer.