Skip to main content
New tool CRON Expression Builder — preview next run times before you schedule Apex. Open the builder →
Diagram illustrating the cause and solution for the common Salesforce Mixed DML error
Apex

Salesforce Mixed DML - Causes and Solutions for Developers

Ever had your code crash because you tried to update a User and an Account at the same time? That's the Salesforce Mixed DML error. This post covers why it happens and how you can avoid it.

The short answer

The Salesforce Mixed DML error happens when you modify setup and non-setup objects within the same transaction. You resolve the conflict with asynchronous Apex patterns, or by isolating the setup DML in its own context inside tests.

Key takeaways Separate setup object DML from non-setup record changes by moving the configuration work into @future methods or Queueable Apex jobs. Wrap User or setup record creation in System.runAs() blocks to isolate execution contexts in unit tests. Use asynchronous paths or subflows in Record-Triggered Flows when the business logic has to update both configuration and data records. Audit triggers on standard and custom objects to find the unexpected background DML that caused the transaction conflict.

Ever hit that wall where your code just stops dead with a nasty error message? The Salesforce Mixed DML error is a classic headache, and it shows up when you try to update a User and an Account in the same transaction. Every developer runs into it eventually, and honestly, it can be a real pain if you don't know why it's happening.

What exactly is Salesforce Mixed DML?

Salesforce splits objects into two camps: setup objects and non-setup objects. A Mixed DML error occurs when you try to perform a DML operation (like insert, update, or delete) on both types in a single transaction. Salesforce basically says, "Wait, I can't let you do that."

Setup objects are things that change the configuration of your org. Think about Users, Groups, Permission Sets, or Queues. Non-setup objects are your actual data, so Accounts, Contacts, Leads, and any custom objects you've built. When you mix them, the system gets worried about transactional integrity. It's a common hurdle and there are easy ways around it.

The error you'll see

Usually, your debug logs will throw something like this:

System.DmlException: DML operation on setup object is not permitted after you have updated a non-setup object type

It's pretty direct, but it doesn't always tell you where the conflict started. I've seen teams spend hours hunting down which trigger or flow caused the initial update that locked the transaction.

Why does Salesforce Mixed DML happen?

It comes down to rollbacks. If a transaction fails, Salesforce needs to roll everything back, and mixing configuration changes (setup objects) with business data (non-setup objects) makes that rollback process incredibly complex for the platform. To keep the org from ending up in a "half-baked" state where a user is created but their account isn't, Salesforce just blocks the whole thing from the start.

A technical diagram showing the conflict between Salesforce Setup objects and Non-Setup objects, represented by a red warning symbol between administrative and business record icons.

A technical diagram showing the conflict between Salesforce Setup objects and Non-Setup objects, represented by a red warning symbol between administrative and business record icons.

Common scenarios for Salesforce Mixed DML

In my experience, this usually pops up in three places. First, when you're automating user creation. Maybe you have a trigger that creates a User record whenever a specific Contact is marked as a "Partner." Second, when you're managing group memberships or queues alongside record updates. And third, and this is the big one, in unit tests.

When I first worked with complex test classes, I'd constantly hit this. I'd create a test User, then try to create some test Accounts for that user to own. If you don't isolate those steps, the test will crash every time. Sometimes you might even be trying to assign records to inactive users and hit this exact issue because of how the records are being handled.

An example of what fails

// This code will fail every single time
Account acc = new Account(Name = 'Cloud Services Inc');
insert acc; // Non-setup DML

User newUser = new User(
    Username = '[email protected]',
    LastName = 'Dev',
    Email = '[email protected]',
    Alias = 'ndev',
    TimeZoneSidKey = 'America/New_York',
    LocaleSidKey = 'en_US',
    EmailEncodingKey = 'UTF-8',
    ProfileId = [SELECT Id FROM Profile WHERE Name='Standard User' LIMIT 1].Id
);
insert newUser; // BOOM: Salesforce Mixed DML error

How to fix the Salesforce Mixed DML error

You have to break the transaction into two pieces. The setup DML needs to happen in its own little bubble, separate from the account or contact updates. The most common way to do that is asynchronous Apex. If you're weighing whether to solve this with code or a tool, my breakdown of Apex vs Flow might help you decide.

  • Use @future methods. Move your setup object logic into a method with the @future annotation. This tells Salesforce to run that code whenever it has a free second, in a completely new transaction.
  • Queueable Apex is a bit more modern than @future. It gives you more control and lets you chain jobs together. If you're dealing with a lot of data, check out my guide on how to stay under asynchronous Apex limits.
  • System.runAs() in tests. In your test classes, wrap your User creation in System.runAs(thisUser) { ... }. This creates a separate context and usually clears up the error for testing purposes.

Pro Tip: If you're using Record-Triggered Flows, you can often avoid this by using the "Run Asynchronously" path or by separating your logic into a subflow that runs after the main transaction finishes.

Fixed code using @future

public class UserHandler {
    @future
    public static void insertUserAsync(String profileId, String email) {
        User u = new User(
            Username = email,
            LastName = 'Async',
            Email = email,
            Alias = 'async',
            TimeZoneSidKey = 'America/Los_Angeles',
            LocaleSidKey = 'en_US',
            EmailEncodingKey = 'UTF-8',
            ProfileId = profileId
        );
        insert u;
    }
}

// In your main code
Account a = new Account(Name = 'Acme Corp');
insert a;

// This moves the setup DML to a separate transaction
UserHandler.insertUserAsync(someProfileId, '[email protected]');

Key takeaways

  • Salesforce Mixed DML happens when you update configuration (Users, Groups) and data (Accounts, Contacts) in one go.
  • The restriction is there to protect the integrity of your org's metadata and data during rollbacks.
  • Async Apex is your best friend here. Use @future or Queueable to split the work.
  • In unit tests, System.runAs() is the standard way to bypass the issue.
  • Always keep an eye on your triggers. A hidden trigger on a standard object is often the "secret" source of a mixed DML conflict.

Hitting this error is a sign that your business logic is getting more complex. Now that you know how to split those transactions, you can get back to building without the platform getting in your way. Just remember to pick the right async tool for the job and your org will stay happy.

Frequently asked questions

What causes a Mixed DML error in Salesforce?

A Mixed DML error occurs when you perform DML operations on both setup objects (such as User or Group) and non-setup objects (such as Account or Contact) in a single transaction. Salesforce blocks this to preserve transactional integrity and prevent complex rollback failures.

How do you fix a Mixed DML error in Apex?

Split the transaction using asynchronous Apex. Move the setup object DML into an @future method or a Queueable Apex job so it runs in a separate transaction from your standard data updates.

How do you resolve Mixed DML errors in test classes?

Wrap your setup record operations, such as User creation, in a System.runAs() block. That creates a separate context for the setup DML and lets you create test data without crashing the test.

Newsletter

One email every Tuesday

New guides, tool updates, and the release-note changes that break things.

No spam. Unsubscribe in one click.

Comments

Loading comments...

Leave a Comment