Flow Performance Optimisation: 5 Things Slowing Your Flows Down
Flows have replaced Workflow Rules and most of the Apex I would have written ten years ago. They're easier to maintain, easier to read in a code review, and easier to hand to an admin team. They're also much easier to write badly, and the platform is generous about letting badly-written Flows run until the day they tip a record into a CPU timeout or an SOQL limit.
I've debugged enough slow Flows on production orgs to recognise the patterns. Five recurring issues account for almost all the performance problems I see. Each one has a fix that takes minutes. Most of them are still in production because nobody knows the Flow is the bottleneck.
1. Get Records Inside a Loop
This is the Flow equivalent of SOQL queries inside an Apex for-loop — and it's the most common performance issue I find. The Flow Builder makes it easy to drop a Get Records element inside a loop, and the platform doesn't warn you when it executes 200 queries because you put it there.
The pattern looks like this:
Loop: For each Order
-> Get Records: Get Account where Id = {!CurrentOrder.AccountId}
-> Decision: Is Account.Type = "Premium"?
-> Update Records: ...
Each iteration runs a separate SOQL query. In a 200-record bulk DML, that's 200 queries — past the SOQL limit, transaction killed.
The fix is to query once, before the loop. Use a Get Records element that pulls all relevant Accounts in one call, then look them up inside the loop using a Collection variable filtered by AccountId. The platform doesn't make this elegant, but the performance difference is substantial. I've seen Flows that timed out on bulk updates run cleanly after this single change.
If you're not sure whether a Get Records is inside a loop, look at the Flow Builder canvas — anything to the right of a Loop element is inside it. The visual cue is what most admins miss.
2. Multiple Update Records Elements That Should Be One
Flows let you stack Update Records elements one after another — each one fires its own DML statement. I've reviewed Flows with five sequential updates on the same record, each updating a different field, each consuming a DML statement.
The fix: combine them. Use a single Assignment element to set all the fields you need, then one Update Records element to commit. The same record, the same fields, one DML instead of five.
This matters because of governor limits — every Update Records is a separate DML. The platform allows 150 DML statements per transaction. A Flow with five updates per record processed in bulk hits the limit at 30 records.
The Update Records elements add up faster than you'd expect, especially when subflows are involved. A parent Flow that calls a subflow which has its own Update Records is two DMLs per record minimum. Look for opportunities to consolidate.
3. Subflows Calling Subflows Calling Subflows
Subflows are useful for reusing logic. They're also a stack-overflow vector when used carelessly. Each subflow call carries overhead — Flow context setup, parameter passing, execution stack management. A Flow that calls a subflow that calls a subflow that calls a subflow runs slower than the same logic written inline, sometimes meaningfully so.
I've seen Flows on a state government org where a 4-level subflow chain was responsible for half the run time, because each level passed the same record collection through and each level had its own initialisation overhead. Refactored to two levels, the Flow ran twice as fast.
The rule of thumb I use: subflows are worth it when the logic is genuinely reused (called from 2+ parent flows) and the subflow is doing meaningful work. A subflow that just contains a single Update Records is overhead for no benefit.
4. Decision Elements Evaluated for Records That Don't Need Them
Flows evaluate every element for every iteration of a loop, even when the work doesn't apply. If you have a Flow that processes 200 Orders but only needs to do something for the 5 Orders with a specific status, the standard pattern is:
Loop: For each Order
-> Decision: Is Order.Status = "Shipped"?
YES -> [do work]
NO -> [skip]
The decision evaluates 200 times. The work runs 5 times. The other 195 iterations are pure overhead.
The faster pattern is to filter the collection before the loop. Use a Get Records element with the WHERE clause, or use a Loop's filtering capability if processing an existing collection. The loop runs only over the records that need work, and the decision element disappears entirely.
The savings here aren't dramatic per record but compound across bulk operations. A Flow processing nightly batch updates of 50,000 records spent 10 minutes on decision evaluation that contributed nothing. After filtering upstream, it ran in 2 minutes.
5. Triggered Flows Doing Work for Records That Don't Need Them
Record-triggered Flows fire on every relevant DML by default. If you have a "When a Case is updated, send a notification" Flow, it fires on every Case update — including the ones where nothing changed that should trigger a notification.
The fix: set entry conditions on the Flow itself, not inside it. The Flow Builder lets you configure the trigger to fire only when specific field changes happen. Use this. It's vastly faster than firing the Flow and then deciding inside whether to do anything.
Trigger: After Case Update
Entry condition: Status changed AND new value = "Resolved"
-> [send notification]
vs.
Trigger: After Case Update
-> Decision: Did Status change?
-> Decision: Is new Status = "Resolved"?
-> [send notification]
The second version fires the Flow on every Case update across the org. The first only fires when the conditions actually match. On a high-volume org, this is the difference between Flow execution being a noticeable load or invisible.
The same logic applies to before-save Flows — set the entry conditions tightly. Every Flow that fires unnecessarily is consuming the platform's compute budget for no benefit.
The Diagnostic Pattern
When a Flow is slow and I don't know why, the first thing I do is enable Flow debug logging and run a representative test. The debug log shows:
- Which elements ran
- How long each took (some elements report this)
- How many records each Get Records returned
- Where DML and SOQL counts increased
Reading the debug log is unglamorous but it's the fastest way to find the actual bottleneck. The expected suspect — a complex decision tree — is often not the issue. The actual issue is usually a Get Records pulling far more records than necessary, or an Update Records hidden inside a loop that wasn't obvious from the canvas view.
For Flows that run in production, I'll also check the Setup Audit Trail and the Flow Interview history to see if there are any orphan execution paths or recursion. Flows that update the same record they're triggered from can recurse, and the platform allows this up to a limit before throwing.
When the Flow Isn't the Right Tool
Flows are good for most automation. They're not the right tool for every automation. The places I'll still reach for Apex:
- Multi-object orchestration where the decision tree gets too complex to read
- Integration callouts with retry logic and error handling
- Heavy computation across large record sets where Apex's collection performance matters
- Scenarios that need fine-grained governor limit management
If you find yourself building a Flow with 30+ elements and 5 nested decisions, that's a signal the logic has outgrown declarative tools. Refactoring it to Apex isn't a defeat — it's recognising that the wrong tool was making the problem harder than it needed to be.
If you're auditing an org and want to know which of these patterns are slowing it down, that's the kind of thing an architecture review catches — Flow performance is one of the more common findings, and the fixes are usually high-impact-low-effort.
The shortest summary: Flows make automation accessible, but they don't make performance free. Bulk safety, entry conditions, and avoiding queries inside loops aren't optional best practices — they're the difference between a Flow that runs and a Flow that times out at 3am on month-end.
Liked this?
New essays in your inbox when I publish. No cadence, no spam.
You might also like
SOQL Patterns That Won't Kill Your Governor Limits
The SOQL antipatterns I keep finding in code review, the patterns that replace them, and the trade-offs nobody explains — from years of production Salesforce.
Read →Async Apex Patterns: Queueable vs Batch vs Future — The Decision Tree
Choosing between Queueable, Batch Apex, and Future methods is the asynchronous decision most Salesforce devs get wrong. Here's the framework I use, and the cases where the obvious choice is wrong.
Read →Data Migration to Salesforce: The Honest Playbook
What actually goes into a clean Salesforce data migration — from field mapping through cutover — and the gotchas that turn one-week migrations into one-month projects.
Read →