Salesforce Upsert Trigger – How Many Times Does It Fire?

How the Salesforce upsert trigger handles records

If you’ve been working in the ecosystem for a while, you’ve probably had to explain how a Salesforce upsert trigger actually works. It’s one of those classic “gotcha” questions that pops up in senior dev interviews and architectural reviews alike. But it’s also a practical reality we deal with every time we’re syncing data from an external system.

The confusion usually starts when people try to figure out if a record counts as an insert, an update, or both. I’ve seen teams over-complicate their code because they were afraid a record would fire through every single trigger context in one go. Let’s clear that up right now.

So, how many times does the trigger execute? The short answer is twice per record: once in the before context and once in the after context. But it only fires for the specific operation that actually happens. If the record is new, it’s an insert. If it exists, it’s an update. It is never both at the same time for the same record.

A technical flow diagram showing the logic branch between an insert and an update operation in a database system.
A technical flow diagram showing the logic branch between an insert and an update operation in a database system.

Decoding the Salesforce upsert trigger execution flow

When you call an upsert, Salesforce does a quick check against the ID or the External ID you’ve provided. It’s basically making a split-second decision for every record in your list. This is why you’ll often see this topic come up in Apex trigger interview questions because it tests whether you understand the underlying DML logic.

Here is how the platform breaks it down:

  • The Insert Path: If the record is brand new, the trigger fires in before insert and after insert. The update contexts are completely ignored for these records.
  • The Update Path: If the record matches an existing ID, the trigger fires in before update and after update. The insert contexts never see these records.

Now, here’s where it gets interesting. If you’re doing a bulk upsert with 200 records and 100 are new while 100 are existing, Salesforce is smart enough to group them. Your trigger will fire for the insert context (for the new guys) and the update context (for the existing ones) within that same transaction. But for any single individual record, it’s a one-way street.

Pro Tip: Always check Trigger.isInsert or Trigger.isUpdate inside your handler classes. I’ve seen developers write generic logic that they thought would run for both, only to realize their “Update” logic wasn’t touching the “Inserted” records during a massive data load.

A simple code pattern for upserts

I usually tell my junior devs to stick to a clean handler pattern. You don’t want to guess what’s happening under the hood. When you’re deciding between Apex vs Flow for these operations, remember that triggers give you this granular control over the insert/update split that’s hard to replicate elsewhere.

trigger AccountTrigger on Account (before insert, after insert, before update, after update) {
    if (Trigger.isBefore) {
        if (Trigger.isInsert) {
            // This only hits for brand new records
        } else if (Trigger.isUpdate) {
            // This only hits for existing records being matched
        }
    }
}

Debugging a Salesforce upsert trigger in the wild

One thing that trips people up is the “twice” phrasing. When someone says a trigger executes twice, they just mean the before and after phases. They don’t mean the record is being saved to the database twice. It’s a single DML event for each record.

But what about those bulk scenarios? If you’re pushing 500 records, Salesforce chunks them into batches of 200. In each batch, the Salesforce upsert trigger will run its phases. If a batch contains a mix of inserts and updates, the trigger will enter the insert blocks for the new records and the update blocks for the existing ones. It’s efficient, but you have to keep your collections straight.

Key Takeaways

  • The Salesforce upsert trigger never treats a single record as both an insert and an update in one transaction.
  • Each record fires exactly twice: once before the save and once after.
  • Bulk upserts can trigger both insert and update contexts in the same transaction if the record set is mixed.
  • Always use context variables like Trigger.isInsert to keep your logic from stepping on its own toes.

At the end of the day, understanding the Salesforce upsert trigger behavior is about knowing how the platform identifies data. Whether you’re using a standard ID or a custom External ID, the trigger follows the same rules. Keep your logic modular, respect the bulk nature of the platform, and you won’t have any surprises during your next big data migration.