Apex Trigger Execution on Upsert — How Many Times Does It Run?

Understanding Trigger Behavior with Upsert

When you perform an upsert operation in Salesforce, records can either be inserted (new) or updated (existing) in the same call. Apex triggers respond to the final DML operation type for each record, so it’s important to understand how triggers fire for mixed operations.

Key Points (SEO-friendly)

On an upsert statement, a trigger will:

  • Fire for insert events for records that don’t match an existing record.
  • Fire for update events for records that match an existing record.
  • If the same upsert call contains both new and existing records, the trigger runs separately for the insert group and the update group. That means the trigger contexts for before insert/after insert and before update/after update will both execute (effectively two executions, grouped by operation).

Short Answer

If all records in the upsert are inserts or all are updates, the trigger executes once (for that event). If the upsert contains a mix of inserts and updates, the trigger will execute twice — once for the insert events and once for the update events.

Example: Mixed Upsert

Imagine you call Database.upsert(listOfAccounts, 'External_Id__c') with a list containing both new accounts (no matching External_Id__c) and accounts whose External_Id__c matches existing records. Salesforce will internally group the records and perform insert and update operations. Your trigger sees two separate contexts:


trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
if (Trigger.isBefore) {
if (Trigger.isInsert) {
// runs for the new records in upsert
}
if (Trigger.isUpdate) {
// runs for the existing records in upsert
}
}
}

Testing and Bulk Considerations

Always design triggers to be bulk-safe. Upsert can pass up to 200 records at once, and you may have both inserts and updates in that batch. Use collections, avoid SOQL/DML inside loops, and check Trigger.isInsert / Trigger.isUpdate to handle logic paths.

Practical Tips

  • Use Trigger.new to access new versions of records and Trigger.old only when Trigger.isUpdate applies.
  • When writing handler classes, separate logic for insert and update into distinct methods. This makes it clear which code executes per trigger context.
  • Write unit tests for mixed upsert scenarios to assert both insert and update behaviors in a single test method.

Conclusion

In summary: an upsert can cause a trigger to execute once or twice depending on whether the batch contains only inserts, only updates, or a mix. For mixed batches, triggers execute twice — grouped by operation type — so handle both contexts in your trigger logic.