Use when making long-running HTTP callouts from Visualforce or LWC that exceed the synchronous callout timeout limit, using the Apex Continuation class for async callout execution. NOT for ordinary synchronous callouts and Named Credential setup — use apex/callouts-and-http-integrations. NOT for choosing between @future, Queueable, and Continuation — use integration/callout-limits-and-async-patterns.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add PranavNagrecha/AwesomeSalesforceSkills --skill continuation-callouts --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Continuation Callouts?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/pranavnagrecha-continuation-callouts)More formats (shields.io, HTML) on the badges page.
---
name: continuation-callouts
description: "Use when making long-running HTTP callouts from Visualforce or LWC that exceed the synchronous callout timeout limit, using the Apex Continuation class for async callout execution. NOT for ordinary synchronous callouts and Named Credential setup — use apex/callouts-and-http-integrations. NOT for choosing between @future, Queueable, and Continuation — use integration/callout-limits-and-async-patterns."
category: apex
salesforce-version: "Spring '25+"
well-architected-pillars:
- Reliability
- Scalability
triggers:
- "callout timeout exceeded in Visualforce controller"
- "long running HTTP request from LWC component"
- "how to make asynchronous callout from Salesforce page"
- "Continuation class apex implementation pattern"
- "parallel HTTP callouts from Apex Visualforce"
- "HTTP callout taking more than 10 seconds from Apex"
tags:
- continuation
- async-callout
- visualforce
- lwc
- http-integration
- long-running-callout
inputs:
- "Apex controller class or LWC Apex method requiring an HTTP callout"
- "External service endpoint URL and authentication details (Named Credential preferred)"
- "Whether single or parallel (up to 3) callouts are needed"
- "Visualforce page or LWC component that invokes the callout"
outputs:
- "Apex Continuation controller or @AuraEnabled method with startRequest/processResponse pattern"
- "LWC component wiring for invokeAction-based continuation (if LWC context)"
- "Checklist confirming timeout config, serializable state, and callback registration"
dependencies: []
version: 1.0.0
author: Pranav Nagrecha
updated: 2026-04-04
---
# Continuation Callouts
Use this skill when a Salesforce page (Visualforce or LWC) needs to call an external HTTP service that is slow enough to make holding the request thread unacceptable, or when you need to fire up to three callouts in parallel without blocking the UI. The Apex `Continuation` class offloads callout execution to Salesforce infrastructure, freeing the request thread while the external service responds.
**What Continuation does and does not buy you.** It is not a way to get a longer timeout. A plain synchronous `Http.send()` can already wait up to 120 seconds — 10 seconds is only the *default* per-callout timeout, and `HttpRequest.setTimeout()` accepts 1 to 120,000 ms in synchronous and asynchronous Apex alike, against a cumulative 120-second budget per transaction. What Continuation buys is that the wait happens on Salesforce infrastructure rather than on the user's blocked request thread, and that the response is handled in a **fresh Apex transaction** with reset governor limits. Choose it for thread occupancy and user experience, not because you think 10 seconds is a ceiling.
---
## Before Starting
Gather this context before working on anything in this domain:
- **Surface type:** Is the entry point a Visualforce page action method, or an LWC `@AuraEnabled` method? The wiring differs. Visualforce uses a controller action that returns a `Continuation` object. LWC uses `invokeAction` from the Lightning Data Service continuation API.
- **Endpoint registration:** The external endpoint must be in the Remote Site Settings (or preferably a Named Credential). Continuation callouts do not bypass this requirement.
- **Most common wrong assumption:** Practitioners assume Continuation works from Apex triggers, Batch Apex, or standard `@future` methods — it does not. Continuation is exclusively for user-initiated requests from Visualforce pages or Lightning Experience components.
- **Platform limits in play:**
- Maximum Continuation timeout: **120 seconds** per callout (configurable via `Continuation.timeout`).
- Maximum parallel callouts per Continuation: **3**.
- Continuation requests do not count toward the org-wide long-running synchronous request governor (since Winter '20, all callouts are excluded from that limit, so this is no longer a differentiating factor — but the 120s ceiling still applies).
- The callback method (`processResponse`) runs in a new Apex transaction with its own governor limits.
---
## Core Concepts
### Concept 1: The Continuation Object and Two-Phase Execution
A `Continuation` request follows a two-phase pattern:
1. **Start phase** — The Visualforce action method (or LWC Apex method) creates a `Continuation` object, adds one or more `HttpRequest` objects to it, sets the callback method name, and returns it. Salesforce intercepts the return value, fires the HTTP request(s) off-platform, and releases the request thread.
2. **Callback phase** — When the external service responds (or the timeout elapses), Salesforce invokes the callback method in a fresh Apex execution context. The callback receives the response via `Continuation.getResponse(label)` and returns the final value to the page.
The Salesforce platform acts as a proxy. The request thread is freed between phases, so no page timeout occurs while waiting for the external service.
### Concept 2: State Passing Between Phases
Because the callback runs in a new transaction, instance variables on a Visualforce controller are not automatically preserved. State must be passed explicitly:
- Set `continuation.state` to any serializable Apex object (String, Map, custom class with `JSON.serialize`-compatible structure) before returning from the start method.
- In the callback, retrieve it from the `state` parameter: `public Object processResponse(List<String> labels, Object state)`.
- **Non-serializable types (Blob, SObject with relationship fields, iterators) cannot be passed as state.** This is a common source of runtime failures.
### Concept 3: Parallel Callouts
A single `Continuation` can encapsulate up to **three simultaneous HTTP requests**. Each call to `continuation.addHttpRequest(req)` returns a unique String label. All requests in the batch fire in parallel. The callback receives a `List<String> labels` and uses `Continuation.getResponse(label)` to retrieve the individual responses. Order of response arrival is non-deterministic; always retrieve by label, not by index.
### Concept 4: Visualforce vs. LWC Invocation
| Context | How to invoke |
|---|---|
| Visualforce | Action method returns `Continuation`. The page uses `action="{!startContinuation}"`. |
| LWC (Aura-compatible) | Import the Apex method and call `invokeAction()` from the LWC JavaScript, which wraps the continuation wire. The Apex method must be annotated `@AuraEnabled(continuation=true)`. |
---
## Common Patterns
### Pattern 1: Single Continuation Callout from Visualforce Controller
**When to use:** A Visualforce page button triggers a callout to a slow external REST API (e.g., a quote-generation service that routinely takes 30–90 seconds).
**How it works:**
```apex
public class QuoteContinuationController {
// State to pass between phases — must be serializable
public String quoteRequestId { get; set; }
public String quoteResult { get; set; }
// Phase 1 — Start the callout, return Continuation
public Continuation startQuoteCallout() {
// Max timeout in seconds (1–120). Default is 30.
Continuation con = new Continuation(60);
// Name of the callback method on THIS class
con.continuationMethod = 'processQuoteResponse';
HttpRequest req = new HttpRequest();
req.setMethod('POST');
req.setEndpoint('callout:QuoteService/api/quotes'); // Named Credential
req.setHeader('Content-Type', 'application/json');
req.setBody('{"requestId":"' + quoteRequestId + '"}');
con.addHttpRequest(req);
// Pass serializable state to the callback
con.state = quoteRequestId;
return con; // Salesforce intercepts and fires the HTTP request
}
// Phase 2 — Callback after callout completes
public Object processQuoteResponse(List<String> labels, Object state) {
// labels[0] is the label for the first (and only) request
HttpResponse res = Continuation.getResponse(labels[0]);
if (res.getStatusCode() == 200) {
quoteResult = res.getBody();
} else {
quoteResult = 'Error: ' + res.getStatusCode();
}
// Return null or a PageReference to navigate after callback
return null;
}
}
```
**Visualforce page binding:**
```xml
<apex:page controller="QuoteContinuationController">
<apex:form>
<apex:commandButton value="Generate Quote"
action="{!startQuoteCallout}"
reRender="resultPanel"/>
<apex:outputPanel id="resultPanel">
<apex:outputText value="{!quoteResult}"/>
</apex:outputPanel>
</apex:form>
</apex:page>
```
**Why not the alternative:** A synchronous `Http.send()` in a Visualforce action method blocks the Salesforce request thread for the whole wait. It will also time out at 10 seconds *if you leave the timeout at its default* — raising it with `req.setTimeout(120000)` avoids the `System.CalloutException: Read timed out`, but does nothing about the blocked thread, which is the actual reason to prefer Continuation here.
---
### Pattern 2: Parallel Callouts from a Single Continuation
**When to use:** A dashboard page needs to aggregate data from three different APIs simultaneously (e.g., ERP inventory, CRM pricing, logistics tracking). Firing them sequentially wastes time.
**How it works:**
```apex
public class DashboardContinuationController {
public String inventoryData { get; set; }
public String pricingData { get; set; }
public String trackingData { get; set; }
private String inventoryLabel;
private String pricingLabel;
private String trackingLabel;
public Continuation startDashboardLoad() {
Continuation con = new Continuation(90);
con.continuationMethod = 'processDashboardResponses';
HttpRequest invReq = new HttpRequest();
invReq.setMethod('GET');
invReq.setEndpoint('callout:ERPService/inventory');
inventoryLabel = con.addHttpRequest(invReq); // store label
HttpRequest priceReq = new HttpRequest();
priceReq.setMethod('GET');
priceReq.setEndpoint('callout:PricingService/current');
pricingLabel = con.addHttpRequest(priceReq);
HttpRequest trackReq = new HttpRequest();
trackReq.setMethod('GET');
trackReq.setEndpoint('callout:LogisticsService/track');
trackingLabel = con.addHttpRequest(trackReq);
// Pass labels as state so callback can retrieve by name
con.state = new Map<String, String>{
'inv' => inventoryLabel,
'price' => pricingLabel,
'track' => trackingLabel
};
return con;
}
public Object processDashboardResponses(List<String> labels, Object state) {
Map<String, String> labelMap = (Map<String, String>) state;
HttpResponse invRes = Continuation.getResponse(labelMap.get('inv'));
HttpResponse priceRes = Continuation.getResponse(labelMap.get('price'));
HttpResponse trackRes = Continuation.getResponse(labelMap.get('track'));
inventoryData = invRes.getStatusCode() == 200 ? invRes.getBody() : 'Error';
pricingData = priceRes.getStatusCode() == 200 ? priceRes.getBody() : 'Error';
trackingData = trackRes.getStatusCode() == 200 ? trackRes.getBody() : 'Error';
return null;
}
}
```
**Why not sequential:** Three sequential Continuation calls would require three separate user interactions or a chained pattern. Using a single `Continuation` with three requests fires them in parallel on Salesforce infrastructure, typically reducing wall-clock latency to the slowest single response rather than the sum of all three.
---
### Pattern 3: LWC with @AuraEnabled Continuation
**When to use:** A Lightning Web Component (not Visualforce) needs to invoke a long-running callout. Available since Summer '19.
**How it works:**
```apex
// Apex controller
public with sharing class ProductSearchController {
@AuraEnabled(continuation=true)
public static Object startSearch(String searchTerm) {
Continuation con = new Continuation(60);
con.continuationMethod = 'processSearchResults';
HttpRequest req = new HttpRequest();
req.setMethod('GET');
req.setEndpoint('callout:ProductCatalog/search?q=' +
EncodingUtil.urlEncode(searchTerm, 'UTF-8'));
con.addHttpRequest(req);
con.state = searchTerm;
return con;
}
@AuraEnabled(continuation=true)
public static Object processSearchResults(List<String> labels, Object state) {
HttpResponse res = Continuation.getResponse(labels[0]);
if (res.getStatusCode() == 200) {
return res.getBody();
}
throw new AuraHandledException('Search failed: ' + res.getStatusCode());
}
}
```
```javascript
// LWC JavaScript — productSearch.js
import { LightningElement } from 'lwc';
import { invokeAction } from 'lightning/actions'; // Aura-compatible wire
import startSearch from '@salesforce/apex/ProductSearchController.startSearch';
export default class ProductSearch extends LightningElement {
results;
handleSearch(event) {
const term = event.target.value;
invokeAction(this, startSearch, { searchTerm: term })
.then(result => { this.results = result; })
.catch(error => console.error(error));
}
}
```
---
## Decision Guidance
| Situation | Recommended Approach | Reason |
|---|---|---|
| Any callout from an Apex trigger, batch, or async context | Synchronous `Http.send()` (raise `setTimeout` past the 10 s default if needed) or Queueable Apex | Continuation is not supported in triggers or async contexts; 10 s is a default, not a ceiling |
| Slow callout (roughly > 5–10 s) from Visualforce | `Continuation` object in controller action method | Only mechanism that frees the VF request thread while waiting |
| Slow callout (roughly > 5–10 s) from LWC | `@AuraEnabled(continuation=true)` + `invokeAction` | LWC equivalent of VF Continuation; same 120 s ceiling |
| 2–3 independent callouts needed simultaneously, from UI | Single `Continuation` with multiple `addHttpRequest` calls | Fires in parallel; reduces latency vs. sequential calls |
| Callout expected > 120 seconds | Polling pattern: trigger async job, poll status endpoint | Platform hard ceiling is 120 seconds; Continuation cannot exceed it |
| From Apex trigger, Batch, Scheduled, @future | Standard synchronous callout or Queueable with callout=true | Continuation requires a user-initiated HTTP request context |
---
## Recommended Workflow
Step-by-step instructions for an AI agent or practitioner activating this skill:
1. Gather context — confirm the org edition, relevant objects, and current configuration state
2. Review official sources — check the references in this skill's well-architected.md before making changes
3. Implement or advise — apply the patterns from Core Concepts and Common Patterns sections above
4. Validate — run the skill's checker script and verify against the Review Checklist below
5. Document — record any deviations from standard patterns and update the template if needed
---
## Review Checklist
Run through these before marking work in this area complete:
- [ ] `Continuation` object timeout set explicitly (1–120 seconds); not left at default 30 if service needs more.
- [ ] `continuationMethod` name matches the actual callback method signature exactly.
- [ ] Callback method signature is `public Object methodName(List<String> labels, Object state)`.
- [ ] All state passed between phases is JSON-serializable (no Blob, no SObject with relationship queries).
- [ ] Each `addHttpRequest` return label is stored and used to retrieve the correct response in the callback.
- [ ] Endpoint is registered in Remote Site Settings or uses a Named Credential.
- [ ] Error status codes handled in the callback (service may return 4xx/5xx on timeout or failure).
- [ ] For LWC: Apex method annotated `@AuraEnabled(continuation=true)`, not plain `@AuraEnabled`.
- [ ] Continuation is NOT used from a trigger, batch, scheduled, or `@future` context.
- [ ] Test class uses `Test.setContinuationResponse()` and `Test.invokeContinuationMethod()` to mock both phases.
---
## Salesforce-Specific Gotchas
Non-obvious platform behaviors that cause real production problems:
1. **Continuation is not supported in triggers or async Apex** — Attempting to instantiate `Continuation` from a trigger, Batch Apex `execute`, Scheduled Apex, or `@future` method throws a runtime exception. The class is only valid in the context of a Visualforce action request or LWC `@AuraEnabled(continuation=true)` invocation.
2. **Non-serializable state silently fails or throws at callback time** — The `state` property is serialized by the platform between phases. Passing an SObject that includes relationship query results, a `Type` reference, or an `Iterator` causes a `System.SerializationException` at callback invocation. Always pass simple scalar types, `Map<String, String>`, or a plain class with `@TestVisible` and primitive fields.
3. **Callback method must be public and on the same class** — `continuationMethod` must be a `public` (not private) method on the same Apex class. It cannot be a static method. If the name is misspelled or the method is private, the callback silently never fires — no exception is thrown at start time.
---
## Output Artifacts
| Artifact | Description |
|---|---|
| Apex controller with `startRequest`/`processResponse` | Visualforce or LWC Apex class implementing the two-phase Continuation pattern |
| Visualforce page with `action` binding | Page XML wiring the action method and output panel rerender |
| LWC component with `invokeAction` | JavaScript component using the continuation-aware import |
| `check_continuation_callouts.py` report | Static analysis of Apex classes for Continuation anti-patterns |
---
## Related Skills
- `apex/callouts-and-http-integrations` — use for ordinary synchronous HTTP callouts, Named Credential setup, and `HttpRequest`/`HttpResponse` fundamentals. There is no duration threshold that hands a callout over to Continuation; choose on request-thread occupancy, not elapsed seconds.
- `apex/async-apex` — use when the core design choice is whether the callout belongs in `@future`, Queueable, Batch, or Scheduled Apex (none of which support Continuation).
- `apex/exception-handling` — use when callback error handling and retry logic need a structured pattern.
- `apex/test-class-standards` — use for mocking Continuation responses in test classes with `Test.setContinuationResponse()`.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!