Salesforce 5 min read

Handling Salesforce Governor Limits in High-Volume REST APIs

Architectural strategies for designing resilient Salesforce REST integrations without triggering CPU timeout or SOQL query limit exceptions.

When external services push high volumes of transactions into Salesforce via custom REST webhooks, developers frequently collide with execution governor limits. The standard synchronous request budget (100 SOQL queries, 10 seconds of CPU time, 10,000 DML rows) can be consumed rapidly during burst traffic.

Here is how we design resilient Salesforce endpoints that scale predictably.

The Core Pitfall: Synchronous Processing of Inbound Payloads

The most common integration mistake is attempting to parse, validate, query, and insert records within a single synchronous Apex @HttpPost method.

When external webhooks fire in bursts—such as an e-commerce platform processing a product drop or an ERP releasing batch invoices—Salesforce worker threads become starved, returning HTTP 500 errors to callers.

// Risky pattern: Synchronous processing inside webhook handler
@RestResource(urlMapping='/inbound-orders/*')
global with sharing class InboundOrderWebhook {
    @HttpPost
    global static void handleOrder() {
        RestRequest req = RestContext.request;
        OrderPayload payload = (OrderPayload) JSON.deserialize(req.requestBody.toString(), OrderPayload.class);
        // Direct database queries and updates inside request thread
        processOrderDirectly(payload); 
    }
}

Pattern 1: Decouple Ingestion from Processing Using Platform Events

The cleanest architectural separation is acknowledging receipt immediately and delegating processing to asynchronous workers.

By defining a High-Volume Platform Event (e.g., Inbound_Order_Event__e), your REST controller merely validates authorization, deserializes the payload, publishes the event, and returns HTTP 202 Accepted:

@RestResource(urlMapping='/inbound-orders/*')
global with sharing class InboundOrderWebhook {
    @HttpPost
    global static InboundResponse handleOrder() {
        RestRequest req = RestContext.request;
        String body = req.requestBody.toString();
        
        Inbound_Order_Event__e event = new Inbound_Order_Event__e(
            Payload__c = body,
            Source_System__c = 'ERP',
            Timestamp__c = DateTime.now()
        );
        
        Database.SaveResult sr = EventBus.publish(event);
        if (!sr.isSuccess()) {
            RestContext.response.statusCode = 500;
            return new InboundResponse(false, 'Event publication failed');
        }
        
        RestContext.response.statusCode = 202;
        return new InboundResponse(true, 'Queued for processing');
    }
}

Why This Works

  1. Instant Response Times: The HTTP request lifecycle drops from 800ms+ to under 50ms, drastically reducing external connection timeouts.
  2. Dedicated Asynchronous Limits: Platform event triggers execute in their own isolated execution context, with higher asynchronous limits (up to 200 SOQL queries and doubled heap space).
  3. Automatic Batching: Salesforce automatically aggregates up to 2,000 platform event messages per trigger invocation, enforcing natural bulkification.

Pattern 2: Idempotent Transaction Records for Replayability

Network hiccups inevitably cause external callers to retry webhooks. Without deduplication, your org will create duplicate orders or lead records.

We always require an External_Transaction_Id__c field on the payload, which is indexed with Unique constraint in Salesforce. Before initiating processing:

Set<String> externalIds = new Set<String>();
for (OrderPayload item : incomingOrders) {
    externalIds.add(item.transactionId);
}

// Bulk check existing transactions in a single query
Map<String, Inbound_Log__c> existing = new Map<String, Inbound_Log__c>();
for (Inbound_Log__c log : [
    SELECT External_Id__c 
    FROM Inbound_Log__c 
    WHERE External_Id__c IN :externalIds
]) {
    existing.put(log.External_Id__c, log);
}

If the ID has already been processed, the system logs an informational notice and skips execution without failing.

Practical Takeaways

  • Never perform heavy business logic inside an @HttpPost endpoint. Acknowledge, persist, and queue.
  • Enforce bulkification from day one. Assume incoming payloads will eventually arrive in arrays rather than single records.
  • Design for idempotency. Retried calls should produce identical system state without manual cleanup.

Written by Koster CX

Independent technical consultancy offering web hosting, modern web design, and custom software and Salesforce solutions.

Get in touch

Continue Reading

Related Articles

View all articles →