Salesforce Dictionary - Free Salesforce GlossarySalesforce Dictionary
All errors
Governor limits

System.LimitException: Too many Email Invocations: 11

Apex caps a single transaction at 10 calls to `Messaging.sendEmail`. The cap counts the **number of method calls**, not the number of recipients — one `sendEmail` with 100 messages is fine; ten calls with 1 message each, then an eleventh = boom.

Also seen asToo many Email Invocations: 11·Too many Email Invocations·email invocation limit

A trigger that sends a confirmation email to every changed contact crashes the moment a batch import touches 11 or more contacts. The log: System.LimitException: Too many Email Invocations: 11. The Apex code looks innocent: one Messaging.sendEmail call per contact in a loop. Eleven calls is over the limit; ten is the cap.

The limit and what it counts

Apex limits a single transaction to 10 invocations of Messaging.sendEmail. The cap counts the number of method calls, not the number of recipients. A single Messaging.sendEmail call with 100 SingleEmailMessage objects in the list is one invocation. Ten such calls, each with a different message structure, is ten invocations and hits the cap on the eleventh.

The platform also caps the total recipient count per transaction (about 5,000 external email addresses per 24 hours, varying by edition), but that's a separate governor with a separate error message. Too many Email Invocations is specifically about the method-call count.

The cap protects shared email infrastructure. Each sendEmail call routes through Salesforce's mail relay; uncapped, a single transaction could attempt to send thousands of mails. The 10-call ceiling forces structural bulkification: build a list, call once.

The broken example

A before update trigger that emails the contact when a custom flag changes:

trigger ContactNotificationTrigger on Contact (before update) {
    for (Contact c : Trigger.new) {
        Contact old = Trigger.oldMap.get(c.Id);
        if (c.Status_Changed__c && !old.Status_Changed__c) {
            Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
            email.setToAddresses(new String[] { c.Email });
            email.setSubject('Your status changed');
            email.setPlainTextBody('Hi ' + c.FirstName + ', your status is now ' + c.Status__c);
            // One Messaging.sendEmail call per contact. Throws on the 11th.
            Messaging.sendEmail(new Messaging.SingleEmailMessage[] { email });
        }
    }
}

The intent is clear: send one email per status-changed contact. The mistake is structural. The trigger calls sendEmail inside the loop, so each contact triggers a separate invocation.

The fix: build a list, call once

Collect every email object inside the loop, then make a single sendEmail call with the full list. The platform sends each message in the list as a separate email; the cap on invocations isn't hit.

trigger ContactNotificationTrigger on Contact (before update) {
    List<Messaging.SingleEmailMessage> emails = new List<Messaging.SingleEmailMessage>();
    for (Contact c : Trigger.new) {
        Contact old = Trigger.oldMap.get(c.Id);
        if (c.Status_Changed__c && !old.Status_Changed__c) {
            Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
            email.setToAddresses(new String[] { c.Email });
            email.setSubject('Your status changed');
            email.setPlainTextBody('Hi ' + c.FirstName + ', your status is now ' + c.Status__c);
            emails.add(email);
        }
    }
    if (!emails.isEmpty()) {
        Messaging.sendEmail(emails);
    }
}

One sendEmail call, regardless of the contact count. The cap is no longer in play.

The fixed example, with delivery checks

A production-shaped notification service that wraps the email pattern:

public class ContactNotificationService {
    public static void notifyStatusChanged(List<Contact> changed) {
        if (changed == null || changed.isEmpty()) return;

        List<Messaging.SingleEmailMessage> emails = new List<Messaging.SingleEmailMessage>();
        for (Contact c : changed) {
            if (String.isBlank(c.Email)) continue;
            Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
            email.setToAddresses(new String[] { c.Email });
            email.setSubject('Your status changed');
            email.setPlainTextBody(buildBody(c));
            email.setTargetObjectId(c.Id);
            email.setSaveAsActivity(true);
            emails.add(email);
        }
        if (emails.isEmpty()) return;

        Messaging.SendEmailResult[] results = Messaging.sendEmail(emails, false);
        for (Integer i = 0; i < results.size(); i++) {
            if (!results[i].isSuccess()) {
                System.debug(LoggingLevel.ERROR,
                    'Email to ' + emails[i].getToAddresses() + ' failed: ' + results[i].getErrors());
            }
        }
    }

    private static String buildBody(Contact c) {
        return 'Hi ' + c.FirstName + ',\n\nYour status is now ' + c.Status__c + '.\n\nThanks,\nSupport Team';
    }
}

sendEmail(emails, false) is the partial-success variant. One bad address doesn't roll back the whole transaction. The SendEmailResult[] array reports per-message success or failure.

setTargetObjectId and setSaveAsActivity(true) cause the email to be logged as a Salesforce Activity tied to the contact, which is usually the right behavior for transactional notifications.

When the cap is too low

If you genuinely need to send 11+ batches of structurally-different emails in one transaction (different templates, different sender, different attachment sets), you need a different design:

Combine compatible messages. Two batches with the same template can usually be merged into one. The fix is to collect compatible messages before calling sendEmail.

Move to async. A Queueable can make 10 sendEmail invocations in its own context. Chaining queueables lets you span any number of distinct email batches.

Use email templates with merge fields. Instead of constructing the body inline, reference a template by id. The platform handles per-recipient personalization, and a single sendEmail call can cover many distinct recipients via merge.

Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setTemplateId(templateId);
email.setTargetObjectId(c.Id);
email.setWhatId(c.AccountId);
emails.add(email);

Each recipient gets a personalized email derived from the same template. One sendEmail call sends them all.

The broader email-governor family

The 10-invocations cap is one of several caps on the email subsystem. Knowing the rest prevents future surprises:

  • Email invocations: 10 Messaging.sendEmail calls per transaction.
  • Single email recipients: 5,000 external addresses per 24 hours (Enterprise+ editions; lower for others).
  • Mass email recipients: 500 per single MassEmailMessage call.
  • HTML/text body size: 32 KB per body.
  • Attachment size: 25 MB total across all attachments per email; individual attachments capped at 10 MB.

MailLimitExceededException is the exception type for the 24-hour external-recipient cap. It fires in addition to (and independently of) the 10-invocations cap.

How transactional emails differ from marketing emails

Transactional emails (the kind your code sends) count against the 5,000-per-24h cap and the 10-invocations cap. Marketing emails sent via Salesforce Marketing Cloud, Pardot, or third-party tools don't count against Apex caps because they route through different infrastructure.

If your team's email volume is high enough to repeatedly hit the Apex caps, the architectural answer is to push notification volume to a dedicated marketing platform. The Apex caps are designed for small-volume transactional sends, not bulk campaigns.

Single email vs Mass email

Salesforce offers two email types:

  • Messaging.SingleEmailMessage: one email per recipient, fully customizable per recipient.
  • Messaging.MassEmailMessage: one logical message sent to many contacts. Uses a template; less customization per recipient.

The 10-invocations cap applies to both. Mass email also has a per-call recipient cap of 500. For 5,000 recipients on one campaign, you'd need 10 mass-email invocations (which hits the 10-invocations cap exactly).

For high-volume notification needs, Mass Email is the right pattern at small scale. Beyond it, move to async or to a dedicated marketing tool.

Diagnostic: check current invocation count

Limits.getEmailInvocations() returns the count used in the current transaction:

System.debug('Email invocations: '
    + Limits.getEmailInvocations() + '/' + Limits.getLimitEmailInvocations());

Drop the debug at suspected hot spots to see whether the count is climbing. The output is the fastest path to identifying a stray sendEmail call inside a loop.

Email and triggers: a careful pairing

A trigger that sends email on every save is reasonable for low-volume objects. For high-volume objects (Lead, Case, Opportunity in a busy org), the trigger fires on every save, batch operations included. A batch update of 200 records means 200 emails per save event.

For high-volume objects, route the notification through a Queueable or a scheduled job that batches notifications hourly or daily. Users get fewer, higher-signal emails. The Apex code runs once per batch, not once per record.

Test patterns

Apex tests can call Messaging.sendEmail but the platform doesn't actually deliver. The recipient never sees anything; the test asserts the call happened.

@isTest
static void notifyStatusChanged_buildsOneEmailCall() {
    List<Contact> contacts = new List<Contact>();
    for (Integer i = 0; i < 50; i++) {
        contacts.add(new Contact(
            FirstName = 'Test',
            LastName = 'Contact ' + i,
            Email = 'test' + i + '@example.com',
            Status_Changed__c = true
        ));
    }
    Test.startTest();
    ContactNotificationService.notifyStatusChanged(contacts);
    Test.stopTest();

    System.assertEquals(1, Limits.getEmailInvocations(),
        'Should make exactly one sendEmail call for any number of contacts');
}

The test asserts the invocation count is exactly 1, regardless of the contact list size. If you regress and reintroduce a loop, the test catches it before deploy.

A pattern for chained notifications

If your notification logic involves multiple sequential sends (e.g., "send an internal alert, then a customer-facing email, then a Slack message"), structure it as a Queueable with one sendEmail per chain link rather than three sends in the synchronous transaction.

public class NotificationChain implements Queueable {
    private List<Contact> contacts;
    private Integer stage;

    public NotificationChain(List<Contact> contacts, Integer stage) {
        this.contacts = contacts;
        this.stage = stage;
    }

    public void execute(QueueableContext qc) {
        if (stage == 0) {
            sendInternalAlert(contacts);
            System.enqueueJob(new NotificationChain(contacts, 1));
        } else if (stage == 1) {
            sendCustomerEmail(contacts);
            System.enqueueJob(new NotificationChain(contacts, 2));
        } else if (stage == 2) {
            sendSlackMessage(contacts);
        }
    }
}

Each chain link uses one sendEmail invocation. The chain produces three sends total but never approaches the 10-invocation cap, since each link is its own transaction.

When the email service is genuinely the wrong tool

If your notification pattern looks like "send 10 distinct emails per record," the Apex email subsystem is fighting you. Two architectural alternatives perform better:

Salesforce Marketing Cloud or Pardot. Designed for high-volume, multi-step email campaigns. The Apex side queues recipient data; Marketing Cloud does the actual sending.

A dedicated transactional email service (Postmark, SendGrid, SES). Apex makes one HTTP callout per batch of messages. The external service handles delivery. Bypasses Apex's email caps entirely.

Both add operational complexity (managing a separate service, monitoring two systems). For genuinely high-volume notification needs, that complexity is justified. For the rest, sticking with Apex sendEmail and respecting the 10-invocation cap works fine.

A nuance with managed packages

Some managed packages send email as part of their normal operation. If your trigger code also sends email, the package's emails plus yours both count against the same 10-invocation cap. A package that sends 4 emails per save plus your code sending 7 per save hits the cap on the eleventh combined call.

Diagnose by checking Limits.getEmailInvocations() at the start of your trigger to see what's been used by other code. If a managed package is consuming a slice of the budget, plan around it.

A maintenance note

Keep an eye on Apex Email Notifications setting in Setup. Some orgs disable it for testing and forget to re-enable. If your code's emails aren't arriving even though the API returns success, check this setting first.

Designing the email content side

Most teams find that the cap forces a healthy redesign of their notification logic. Three principles tend to emerge from the redesign:

Consolidate where possible. "Send an email per record change" often becomes "send one summary email covering all changes." Users prefer the consolidated version anyway; the cap encourages it.

Defer routine notifications to async batches. Real-time email per save is rarely necessary. A scheduled hourly digest is faster for the trigger context and easier on the recipient's inbox.

Use templates instead of inline body building. Templates centralize the wording and let admins update the message without code changes. The save-per-template approach also makes localization easier.

These principles aren't just about the cap. They're about how high-quality notification systems behave in general. The cap nudges you toward the right design.

A real-world refactor

A team had a trigger that sent one email per opportunity-closed event. The trigger handled batches of 200 opportunities by sending 200 emails. They hit the 10-invocation cap when the trigger started getting invoked from a bulk import.

The fix:

  1. Replaced the trigger's inline sendEmail with a System.enqueueJob(new OpportunityClosedNotifier(opps)).
  2. The queueable collected all opps and called sendEmail once with the list.
  3. Added a setTemplateId reference so the body lived in a managed Email Template instead of Apex source.
  4. Added partial-success handling so one bad address didn't fail the whole batch.

The refactor took an afternoon. The notification pattern stayed identical from the recipient's perspective. The cap incident disappeared. Bonus: admins could now edit the template wording without redeploying code.

Further reading from Salesforce

Related dictionary terms

Share this fix

Share on LinkedInShare on X

Related Governor limit errors