How Continuation Works in Apex

What Is a Continuation in Apex?

Let’s start simple. A continuation in Apex is Salesforce’s way of letting you call out to external services without making your users wait around. Normally, when you make an HTTP callout in Apex, it’s synchronous - you send a request, wait, and the user sits twiddling their thumbs until it comes back. Continuations flip that on its head.

Instead of holding everything up, a continuation says, “Hey, Salesforce, I’ll handle this later. Go do your thing, and I’ll catch up when the response gets here.” Think of it like ordering coffee and stepping aside until your name’s called, rather than waiting awkwardly in front of the cashier.

Here's a bare-bones example:

public class ContinuationExample {
    /*
     * This method is exposed to Aura and uses Continuation for async callout.
     *
     * Note: The Continuation pattern is used for long-running callouts. The platform will automatically
     * resume execution by calling the method specified in continuationMethod when the response is ready.
     */
    @AuraEnabled(Continuation = true)
    public static Object makeCallout() {
        /**
         * Creates a Continuation object with a 5-second timeout.
         * The 5-second timeout parameter specifies the maximum amount of time (in seconds)
         * that the server will wait for the asynchronous callout to complete before timing out.
         * This helps ensure that long-running callouts do not block server resources indefinitely.
         */
        Continuation cont = new Continuation(5);
        /**
         * The continuationMethod property tells the platform which method to invoke
         * when the asynchronous callout completes. This must match the name of your response handler method.
         * Here, it is set to 'processResponse', so the platform will call processResponse when the HTTP response is ready.
         * This is a critical step for Continuation to work correctly.
         */
        cont.continuationMethod = 'processResponse';
        System.debug('Created Continuation with 5-second timeout');
        HttpRequest req = new HttpRequest();

        // Set the endpoint for the callout (simulates a delayed response)
        req.setEndpoint('https://forceshark.com/api/v1/hello-world/continuation-demo?delayMs=10000');
        req.setMethod('GET');
        System.debug('Prepared HTTP request for endpoint: ' + req.getEndpoint());
        
        /**
         * When you add an HTTP request to the Continuation, you receive a request label (a String).
         * This label uniquely identifies the request and is used later to retrieve the corresponding response.
         * You must use the same label in Continuation.getResponse(label) to get the correct HttpResponse.
         */
        String requestLabel = cont.addHttpRequest(req);
        System.debug('Added HTTP request to Continuation. Request label: ' + requestLabel);
        
        // Store user info and request label in a map, then serialize to JSON for cont.state
        Map<String, Object> stateMap = new Map<String, Object>{
            'userId' => UserInfo.getUserId(),
            'userName' => UserInfo.getName(),
            'requestLabel' => requestLabel
        };
        /**
         * The cont.state property allows you to store any serializable object or value
         * that you want to persist between the initial callout and the response handling.
         * For example, you might store user context, parameters, or any data you need
         * when processing the response.
         * In a real scenario, you would access cont.state in the response handler to restore context.
         */
        cont.state = JSON.serialize(stateMap);
        System.debug('Set cont.state to serialized user info and request label: ' + JSON.serializePretty(stateMap));
        
        // Return the Continuation object to the platform, which will resume when the response is ready
        return cont;
    }

    /*
     * This method processes the response from the async callout.
     *
     * Note: @AuraEnabled(Cacheable=true) is used for methods that do not modify data and can be cached on the client side.
     * However, processResponse retrieves dynamic data from an async callout, so it should NOT be marked as Cacheable=true.
     */
    @AuraEnabled
    public static Object processResponse(String stateJson) {
        // Deserialize the state JSON to retrieve user info and request label
        Map<String, Object> stateMap = (Map<String, Object>) JSON.deserializeUntyped(stateJson);
        System.debug('Deserialized cont.state: ' + stateMap);

        String requestLabel = (String) stateMap.get('requestLabel');
        System.debug('Using requestLabel from state: ' + requestLabel);
        
        HttpResponse res = Continuation.getResponse(requestLabel);
        System.debug('Received HTTP response. Status: ' + res.getStatus());
        System.debug('Response body: ' + res.getBody());
        return res.getBody();
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<RemoteSiteSetting xmlns="http://soap.sforce.com/2006/04/metadata">
    <disableProtocolSecurity>false</disableProtocolSecurity>
    <isActive>true</isActive>
    <url>https://forceshark.com</url>
</RemoteSiteSetting>

In this code, we send off a request and register a handler to process the response when it comes back.

The Problem with Traditional Callouts

You ever waited on a slow API? Painful, right? That’s exactly what happens with traditional Apex callouts. Since Apex runs on Salesforce's multi-tenant architecture, there's a tight leash on how long your operations can run. A callout that's too slow can throw a timeout and crash your process - bad news for your user and your code.

Synchronous callouts tie up the server thread. It’s like calling a friend who takes forever to answer while your boss stares at you, waiting. Not ideal.

This becomes an even bigger problem in Lightning components or Experience Cloud pages where user experience matters. Every second of delay increases the chance that a user clicks away.

How Continuations Solve These Problems

Continuations make your apps feel snappier and smarter. Instead of waiting, your logic keeps flowing. Salesforce frees up the processing thread while your external callout does its thing in the background. Once the response comes back, the process picks up exactly where it left off.

It's kind of like putting your laundry in the machine and going off to cook dinner. When it buzzes, you're ready to fold. No time wasted.

Technically, it works like this:

  • The callout is sent using a Continuation object.
  • The server thread is released.
  • When the external system replies, Salesforce rehydrates the Apex transaction and processes the response using a callback.

It’s elegant. Efficient. And your users? They’re not staring at spinning wheels.

Real-World Scenarios Where Continuations Shine

Let’s talk about where this really pays off.

Complex Integrations with Slow APIs - Ever had to deal with a payment processor that takes 10 seconds to return? You can't afford that kind of lag in your UI. Continuations let you send that payment request, render a “Processing Payment…” message, and get on with your life.
Government or Public APIs - These are notoriously slow. Think tax services, verification portals, or compliance checks. You don’t want these bottlenecks killing your app’s performance. Continuations keep things smooth.
Chatbots and Real-Time Interfaces - Imagine a user chatting with a bot that needs to ping an AI model or fetch user data from a legacy system. That delay can break the flow. But with continuations, the conversation can keep going without hitches.
Partner API Requests in Experience Cloud - Guest users in a portal waiting on a callout? That’s UX suicide. Continuations let you keep your portal fast, even when the data isn't.

Tips for Using Continuations Effectively

Always Handle Timeouts: Continuations wait up to 120 seconds, but don’t rely on hitting that limit. Have logic for slow APIs.
Use Unique Labels: If you have multiple callouts, make your request labels distinct. It helps avoid confusion in getResponse().
Stay Stateless: Remember that your Apex class gets serialized and deserialized. Avoid storing sensitive or heavy data in the instance variables.
Secure Your Callouts: Always set named credentials or use a custom AuthProvider to protect your endpoints.

Common Pitfalls to Avoid

Calling Continuation Methods from Triggers Big no-no. Continuations are designed for UI-triggered actions, not backend logic.
Using Them Outside Lightning Contexts Continuations only make sense when there's a user waiting. Don’t misuse them in scheduled jobs.
Incorrect Endpoint URLs Make sure your endpoints are whitelisted in remote site settings or defined as named credentials.
Hardcoding Labels Avoid hardcoded request labels if your flow is dynamic - use a map or generated UUIDs for tracking.

Conclusion

At the end of the day, Continuations in Apex aren’t just a fancy trick - they’re a vital tool for building modern, scalable, user-friendly apps on Salesforce. They take the pain out of slow callouts and bring back control to your code. If you’re serious about improving performance, especially in Lightning apps and portals, it's time to master this pattern.

Think of Continuations as your backstage pass to smoother UI experiences. They won’t solve every problem, but when the use case is right, they can be game-changing.

So next time your Apex starts stalling over an HTTP request, ask yourself - why wait when you can continue?