# Why your flow is slow

> Ordered by what to measure rather than what to fix, because the expensive mistake is not a slow flow. It is a week spent optimising the part of the transaction that was never the problem.

- Source: https://synconai.com/insights/flow-performance-optimisation
- Publisher: SynconAI (https://synconai.com)
- Desk: Platform & Architecture
- Author: SynconAI Architecture Team, Solution & technical architecture
- Published: 14 May 2026
- Updated: 29 August 2026
- Reading time: 11 minutes
- Topics: Flow, Performance, Governor limits, Debugging, Automation

## Key points

- A slow flow and a slow transaction are different findings. Separate them before you change a single element.
- Queries and record updates inside a loop remain the most common cause, and the only reliable evidence is a bulk run rather than a single-record test.
- Recursion rarely announces itself. It shows up as the same automation appearing repeatedly in one log, usually because a flow updates the field that retriggers it.
- Entry conditions are the cheapest optimisation available, because the fastest work is the work that never starts.

---
The ticket usually reads the same way. Saving an Opportunity takes eight seconds. The data load failed. Someone in sales has started counting out loud while the spinner turns. Attached to the ticket is a flow, because a flow is the visible thing, and a flow is what somebody changed most recently.

That attachment is a guess, and it is wrong often enough to be worth testing before anyone opens Flow Builder. The order below is therefore a measurement order rather than a fix order. Every section names the evidence that confirms or eliminates a cause, because the expensive failure in performance work is not a slow flow. It is four days spent optimising something that was never on the critical path.

## A slow flow and a slow transaction are different findings

When a user saves a record, one transaction runs. Inside it sit before-save flows, triggers, after-save flows, validation rules, workflow rules nobody has retired, roll-up recalculations, sharing recalculations, and whatever a managed package has subscribed to on that object. They share one budget for queries, for record operations and for processing time. None of them gets an allocation of its own.

Two consequences follow, and both change how you read the symptom.

The first is that your flow can be entirely reasonable and still be the automation that fails, because it happened to run last. Limits are cumulative across the transaction, so the automation that trips the ceiling is frequently not the automation that consumed the budget. A flow doing one query in a transaction where a package has already exhausted the per-transaction query allowance is a victim, and rewriting it will not help.

The second is that the error message misleads. The processing time limit reports itself in terms that name Apex even when the transaction contains none of your own Apex at all. Teams read the error, conclude the problem must sit in code, and go looking in the wrong place. The error is telling you the transaction ran out of processing time. It is not telling you which participant spent it.

::: note
Before you accept that a flow is slow, get a straight answer to one question: is this transaction slow for every user, on every record, or only under a specific condition? A save that is slow only during a data load, only for one record type, or only for one profile is telling you something about volume, branching or sharing, not about the flow as a whole.
:::

## Measure in this order

Optimisation without measurement is a guess dressed up as engineering. The sequence below moves from cheapest evidence to most expensive, and most investigations end at step two or three.

1. **Reproduce it deliberately.** One save, one user, one record you can save again. If you cannot make it slow on demand, you cannot prove you fixed it.
2. **Capture a debug log for that save.** Raise the workflow and Apex categories so the log shows automation in the order it ran, with limit usage accumulating as it goes. This is the only artefact that shows the whole transaction rather than your part of it.
3. **Read the log as a list of participants.** Write down every trigger, flow and package that appears. Note where the cumulative counters jump. That list, not your intuition, decides who to look at.
4. **Run the flow in Flow Builder with debug details on.** This shows the path taken through the elements and what each one returned, which tells you which branch actually executes and how many records each query brought back.
5. **Repeat under volume.** Load records through the same path in a full sandbox. Faults that scale with record count are invisible at one record and unmissable at two hundred.

Only after step five should anybody edit an element. The [Salesforce developer documentation](https://developer.salesforce.com/) is the authority on what the current limits are and how they are counted, and it is worth checking rather than remembering: the figures move by release, and a number somebody memorised three years ago is a liability in a triage session.

## Triage: symptom to the next measurement

The right-hand column is deliberately not a fix. It is the next thing to measure, chosen to be cheap and to eliminate a cause quickly.

| What you observe | What it usually means | Next measurement |
| --- | --- | --- |
| Save is slow for every user on every record | Something runs unconditionally on the save | Debug log, list every participant in the transaction |
| Fine on one record, fails on a data load | Work that scales with record count, typically inside a loop | Run two hundred records through the same path in a sandbox |
| Query limit error, flow does few queries | Cumulative usage across the transaction | Read cumulative counters in the log, not the last element |
| Processing time error naming Apex, no Apex of yours runs | Total transaction time, participant unknown | Compare log timestamps between automation entries |
| The same automation appears repeatedly in one log | Recursion, usually a flow updating its own trigger field | Compare entry conditions against the fields the flow writes |
| Slow only for one profile or role | Sharing recalculation rather than the flow | Reproduce as a system administrator and compare |
| Slow only since a recent release | New automation or a changed package | Diff automation on the object against last known good |
| Flow runs on almost every save of the object | Entry conditions too broad | Count the saves where the flow does no useful work |

## Queries and record changes inside loops

This is the oldest fault on the platform and still the most common. A **Loop** element iterates a collection. Inside the loop sits a **Get Records**, or an **Update Records**, or a subflow that does one of those on your behalf. Each iteration performs its own operation.

At one record it is correct and fast. At two hundred it is two hundred operations against a shared budget, and the transaction fails. The pattern is identical to the query-inside-a-loop fault in Apex, which is why the same team that would catch it instantly in a code review can walk past it on a canvas: the loop is drawn rather than written, and the cost is not adjacent to the element that causes it.

The shape that works is unglamorous. Query once before the loop and bring back everything the loop will need. Iterate over the collection in memory. Build a second collection of the records that need changing. Perform one update after the loop with that collection. The flow gets longer and the transaction gets cheaper, which is the trade you want.

Two variants hide from a casual read. A subflow inside a loop looks like one element and may contain several operations. An action inside a loop, including anything that reaches outside the org, is the same fault with a worse failure mode, because external latency does not respect your budget at all.

::: warn
A single-record test cannot find this class of fault. It is the one case that never fails. If the only evidence that a flow performs acceptably is somebody editing one record by hand, that flow has not been tested for performance at all.
:::

## Recursion, including the update that retriggers itself

The circular version is easy to state. A record-triggered flow on Account updates a field on the Account. That update is a save. That save meets the flow entry conditions. The flow runs again.

The platform has protections that stop this becoming genuinely endless, but protection against an infinite loop is not the same as not paying for the repetition. What you get is a transaction that does its work several times over, consuming budget on every pass, and a save that is several times slower than the logic in it justifies.

The evidence is specific and easy to read once you know to look for it. In the debug log, the same automation appears more than once for a single user action. That is the finding. Everything after it is confirming why.

The cause is nearly always a mismatch between what the flow writes and what triggers it. Put the two lists side by side: the fields the flow updates, and the fields or conditions in its entry criteria. Any overlap is a candidate. If the flow updates a field on the same record that started the transaction, the answer is usually a before-save flow, which sets the value on the record being saved without a second save at all.

The indirect version is harder. A flow updates a Contact. A Contact flow updates the Account. An Account flow updates the Contact. Nobody built a loop; three people built three reasonable flows across two years. This is the case where the log earns its keep, because no amount of reading any one flow in isolation will reveal it and the log shows it in a single read.

## Work that should never have started

The cheapest optimisation available is not making the flow faster. It is not running it.

Record-triggered flows offer entry conditions, and there is a meaningful difference between running whenever a record is updated and still meets a condition, and running only when the record has newly come to meet it. The first fires on every subsequent save of a record that already qualifies, which for a busy object can be most saves in the org. The second fires on the transition, which is usually what the requirement actually described.

The diagnostic is a counting exercise rather than a technical one. For a representative period, estimate how often the flow runs and how often it does anything at all. A flow that runs on every save and takes its first decision branch straight to an end point most of the time is spending the org's budget to decide it has nothing to do. Tightening the entry conditions removes that cost entirely, and it is a change to one screen rather than a redesign.

Two related habits belong here. Retrieve only the fields the flow uses rather than everything on the object, and filter in the query rather than retrieving broadly and discarding with a decision element. Both reduce work the platform has already done by the time your decision runs.

## The cost of doing what the platform would do anyway

Some flows are slow because they were asked to do something the platform performs more cheaply by other means, usually because a flow was the tool the builder was fluent in.

A flow that reads a related record on every save to copy a value is doing repeatedly what a formula field evaluates on demand. A flow that recalculates a total across child records is competing with a roll-up. A flow that queries related records to decide whether a save is allowed is doing the work of a validation rule, and doing it later in the order of execution than the validation rule would have.

None of this makes flows the wrong tool. It makes them the wrong tool for that specific job, and the pattern to look for is a flow whose entire purpose is keeping one field in step with another. Those are the ones to test against a declarative alternative first. Where the logic is genuinely complex, the honest comparison is between a flow and an invocable Apex action, and that is an ownership question as much as a performance one. How to make it, and how to record it, is set out in [Flow or Apex? Write the decision down before you build](/insights/flow-or-apex-write-the-decision-down).

Timing is the other lever the platform gives you. Not everything on a save has to happen during the save. Work nobody is waiting for, notifications, integrations, downstream updates that are not read immediately, can move to an asynchronous or scheduled path. The total work is unchanged. The user stops waiting for it, which is what the ticket was actually about. The [Salesforce architect guidance](https://architect.salesforce.com/) on transaction design is the reference to read before moving anything, since asynchronous work changes error handling as well as timing.

## Confirming the flow is actually the cause

Before the fix goes in, close the loop. Three checks, in order, each of them quick.

**Isolate it.** Deactivate the flow in a sandbox and reproduce the same action. If the transaction is still slow, the flow was a passenger. This is blunt, it is only safe in a sandbox, and it settles the argument faster than any amount of reading.

**Compare timestamps.** The log carries timing between entries. The gap between the start and the end of each participant tells you where the seconds went. Read the whole transaction, because the participant that failed and the participant that spent the budget are frequently different.

**Check what changed.** Performance that degraded on a date rather than gradually points at something installed, activated or upgraded. Compare the automation on the object against the last configuration you know performed acceptably. A package upgrade that added a subscriber to your busiest object is a common and easily missed answer, and it is the kind of change worth catching in the release cycle rather than in production, which is the argument for a standing [release triage habit](/insights/release-notes-triage-in-thirty-minutes).

## What to do with what you measured

Fix one thing, then measure again with the same method that found it. Two changes at once produce an improvement you cannot attribute, and unattributable improvements have a habit of quietly reverting.

Then write down what the measurement showed, in the flow description and in the ticket. The next person to inherit this object will otherwise repeat the whole investigation, starting from the same guess: that the flow is slow because a flow is the visible thing. Anything approaching a real inventory of automation per object, and the conventions that keep it legible as the org grows, sits alongside this work rather than inside it, and we treat it separately in [Flow in a large org: conventions that hold at a hundred flows](/insights/flow-best-practices-large-orgs).

One last discipline. Check the current limits in the [platform documentation](https://www.salesforce.com/platform/) at the start of each investigation rather than relying on a figure you are confident about. Limits change by release, counting rules change with them, and confidence in a remembered number is the most reliable way to spend a day proving the wrong thing.
