Not verified hands-on. This article was written from primary documentation rather than reproduced in an org, so it carries no tested-release badge. If you run it and something differs, say so in the comments and it gets tested and corrected.

Flow bulkifies operations across interviews — when the same element runs for 200 records in one transaction, it becomes one query or one DML statement. It bulkifies nothing inside a Loop. A Get Records or Update Records element sitting between the Loop and its end point runs once per iteration, so 200 records costs you 200 queries or 200 DML statements against limits of 100 and 150.

The fix is the same shape every time: loop to build a collection, then do a single DML on that collection after the loop has closed.

Bulkification in FlowDML inside a loop against a collection assigned in the loop and saved once after it.DML inside the loopGet Records returns 200 child rowsLoop over the collectionUpdate Records inside the loop200 DML statements — fails at 151Flow bulkifies its Get and Update elements, butonly when they sit outside the loop.Collect, then one DMLGet Records returns 200 child rowsLoop, and Assign each changed row into acollection variableAfter the loop: one Update Records on thecollection1 DML statementThe loop still runs 200 times; only thedatabase work is batched.The same rule applies to Get Records: one query before the loop beats one query per iteration.
The loop still runs 200 times either way. Only the database work moves.

What Flow bulkifies for you

When a save affects multiple records, the platform creates one flow interview per record — 200 interviews for a 200-row Data Loader batch. Those interviews are not executed in isolation. Operations of the same type, executed by the same element, are grouped together, up to 200 record changes per transaction. One Update Records element reached by all 200 interviews becomes one DML statement, not 200.

This applies to record operations, to actions such as email alerts and quick actions, and to invocable Apex called from the flow. It is the reason a well-built record-triggered flow survives a Data Loader import without anybody tuning it.

The grouping is per element, and that matters. If a Decision sends 120 interviews down one branch to Create Records element A and 80 down another to Create Records element B, you get two DML statements, not one. Two identical Create Records elements on two branches cost twice what one shared element after the branches would.

What it never bulkifies: anything inside a Loop

A Loop iterates one item at a time within a single interview. There is no set of parallel interviews to group, so every data element inside the loop executes on its own. This is the failure, and it is invisible at your desk because you tested with one record.

Design 1 record 200 records Limit
Update Records inside the Loop 1 DML 200 DML statements 150 — fails
Get Records inside the Loop 1 SOQL 200 queries 100 sync — fails
Assignment inside the Loop, Update Records after it 1 DML 1 DML statement, 200 rows 150 and 10,000 rows — fine

Note what happens with the loop-DML version at 200 records: it does not fail on record 200. It fails on record 151, having already written 150. Flow rolls the transaction back, so the user sees an error and no saved work, but the debug log shows a long run of successful DML before the exception. That pattern (lots of successful work, then a limit) is the signature of a DML inside a loop.

The pattern, with element names

To update a collection of related records:

  1. Get Records — outside any loop. Filter as tightly as you can here. Every record you do not retrieve is a record you do not iterate over.
  2. Create a record collection variable of the same object type. This is the accumulator.
  3. Loop over the collection from Get Records.
  4. Assignment inside the loop — set the field values on the loop variable. This is in-memory only. No DML, no query.
  5. A second Assignment inside the loop — add the loop variable to your record collection variable using the Add operator.
  6. After the loop’s For Each path closes, a single Update Records element set to Use the IDs and all field values from a record or record collection, pointed at your collection variable.

Two Assignment elements look redundant until you have debugged it once. The loop variable is a copy: modifying it does not touch the source collection, so without step 5 you end up updating nothing. Order matters too: assign the fields before you add to the collection, or you add the unmodified copy.

The limits you are actually spending

Flow does not get its own limit budget. It spends the same per-transaction Apex governor limits as everything else running in that save:

Limit Synchronous Asynchronous
SOQL queries 100 200
Records retrieved by SOQL 50,000 50,000
DML statements 150 150
Records processed by DML 10,000 10,000
CPU time 10,000 ms 60,000 ms

Shared means shared. Your flow’s 40 queries sit in the same budget as the Apex triggers on the object, the other three flows, the validation rules, and any managed package listening to the same save. A flow that uses 60 queries is not safe just because 60 is under 100. It is one package install away from being the thing that breaks.

The 10,000 DML rows limit is the one that bites correctly-built flows. A single Update Records on a collection is one DML statement, so statements are no longer your constraint, but the rows still count. Loop over 200 Accounts, retrieve 60 Contacts each, and update all of them: one DML statement, 12,000 rows, and a failure.

CPU time is worth understanding precisely, because it is the limit people misdiagnose. It counts application-server compute: flow element execution, Apex, validation rules, managed package code. It excludes the database time for your SOQL and DML, and time waiting on a callout. A flow that is slow because of a badly filtered query will show wall-clock slowness without moving CPU much. A flow that loops 40,000 times will blow the CPU limit while barely touching the database.

The tradeoff nobody mentions

Collection DML makes the batch atomic. One invalid record. A validation rule failure, a required field, a lock contention, and the whole Update Records element fails, taking every other record with it. Per-record DML inside a loop, for all its limit problems, fails one record at a time.

If partial success genuinely matters, you have two honest options. Add a fault path from the Update Records element so the failure is handled deliberately rather than surfacing as a raw error to the user. Or split the work: filter the collection into a “known good” set before the DML, using a Collection Filter element, so the records you expect to fail never enter it. Neither gives you the per-record isolation that Apex’s partial-success DML does. Say so out loud in the design review rather than discovering it in production.

Moving the work to an asynchronous or scheduled path resets the limits into a fresh transaction, which is the standard answer at volume. What you give up is immediacy: the records are not updated when the user’s save returns, so anything downstream (a screen, a report the user refreshes, a second automation) sees the old values for a while. Eventual consistency is the price, and it is a fair price, but it is a price.

Confirming it works at 200

Build with one record, verify with 200. Prepare a CSV of 200 rows that all satisfy your entry conditions, load it through Data Loader with a trace flag active (Setup → Debug Logs, Workflow at FINER, Apex Code at DEBUG), then read the log for SOQL_EXECUTE_BEGIN and DML_BEGIN counts. Two or three of each for a 200-record batch is a bulkified flow. Two hundred of either is a loop problem, whatever the flow diagram looks like.

Check LIMIT_USAGE_FOR_NS at the end of the log as well. It prints the actual consumption against each limit for the transaction, which tells you how much headroom you have left for the next package somebody installs.

Frequently asked

Is 200 the batch size for record-triggered flows?

200 is the grouping size for bulkified operations within a transaction. A larger load is split into multiple transactions of up to 200, each with its own fresh limit budget. This is why a 10,000-row import can succeed while a single 200-row batch fails. The limits are per transaction, not per job.

Does a Get Records inside a loop really run every iteration?

Yes. There is no caching and no deduplication. Ten iterations against the same filter is ten queries. Retrieve everything you need in one Get Records before the loop and filter the collection in memory instead.

What if I need a related record per iteration and cannot pre-fetch?

You usually can pre-fetch: query the child object with a filter of ParentId In your collection of parent IDs, then use a Collection Filter inside the loop to narrow to the current parent. Where the matching genuinely needs a keyed lookup, Flow has no map data type, and invocable Apex is the right call rather than a nested loop.

Do subflows share the limits?

Yes. A subflow runs in the same transaction as its parent and draws on the same budget. Splitting a large flow into subflows organises the canvas. It does nothing for limits.

Are before-save flows cheaper?

Considerably, for same-record field updates. There is no second DML and no second trip through the save order. They cannot create or update other records, so the bulkification pattern above is an after-save concern.

Does an Update Records element on a collection count rows I did not change?

Every record in the collection you pass is a DML row, whether or not its field values differ. Filter the collection down to records that actually need writing before the DML: it keeps you under the 10,000 row limit and avoids triggering downstream automation on records that did not change.


Reference: “Flow Bulkification in Transactions”, “How Flows Run in Transactions”, and “General Flow Limits”, Salesforce Help; Apex Developer Guide, Execution Governors and Limits.