REQUEST_RUNNING_TOO_LONG: Your request was running for too long
An API request hit the platform's hard cap on request duration. For most APIs, 120 seconds. The platform interrupts; the client gets this error. Almost always means the work needs to move to async (Bulk API, Batch Apex, queueables) or be split into smaller requests.
Also seen asREQUEST_RUNNING_TOO_LONG·request was running for too long·REQUEST_RUNNING_TOO_LONG: Your request
A middleware service syncs daily updates from Salesforce to a downstream data warehouse. The job runs at 2 AM and processes the previous day's changed Opportunities. For nine months the job finished in under twelve minutes. Last night the job died at the 10-minute mark with REQUEST_RUNNING_TOO_LONG: Your request was running for too long, and has been stopped. The team woke up to a data freshness alert. The downstream BI team has been promised the new figures by 8 AM.
What the platform is checking
Salesforce enforces a maximum wall-clock duration on synchronous API requests. The exact cap depends on the API and the operation, but typical values include 120 seconds for a SOAP or REST query, 600 seconds for a Bulk API job, and 60 seconds for a single synchronous Apex transaction. When a request exceeds the cap, the platform returns REQUEST_RUNNING_TOO_LONG and closes the connection.
The cap protects the shared infrastructure. A single client that holds a connection open for half an hour blocks resources that other tenants need. Capping at a reasonable duration forces clients into appropriate patterns for long-running work (async APIs, Bulk API jobs, queueable Apex).
The cause is usually one of three things. The request is genuinely too large; the query or DML touches more data than fits in the cap. The query is inefficient; even a small result set takes too long because of a missing index, a non-selective filter, or a join across large objects. The downstream system is slow; an outbound callout from Apex spends most of its time waiting for an external response.
The error fires at the client side as well as inside the platform. A client integration with a default HTTP timeout shorter than the platform's may see its own timeout first. Aligning client and platform timeouts gives a consistent error surface.
The broken example
A middleware service that queries changed Opportunities via the REST API:
import requests
session = get_salesforce_session()
url = f"{session['instance_url']}/services/data/v60.0/query"
soql = """
SELECT Id, Name, Amount, StageName, CloseDate, AccountId,
Account.Name, Account.Industry,
(SELECT Id, Subject FROM Tasks),
(SELECT Id, Subject FROM OpenActivities),
(SELECT Id, Email FROM OpportunityContactRoles)
FROM Opportunity
WHERE LastModifiedDate >= YESTERDAY
ORDER BY LastModifiedDate ASC
"""
response = requests.get(url, params={'q': soql}, headers=session['headers'], timeout=600)
The query selects parent and three child relationships in one go. On a day with 50,000 changed Opportunities, the query engine has to materialize the parent records, then for each one resolve three subqueries. The total result size and the time to assemble it both grow. Eventually the request hits the 120-second cap and the platform aborts.
A second shape: a Bulk API V1 job that submits 10 million records in a single batch. Each batch has a maximum runtime. The platform processes records until the batch hits the cap, then marks the batch as failed.
A third shape: a streaming HTTP request from an external system that holds the connection open while waiting for events. The platform closes the connection after a fixed duration even though events are still being delivered.
The fix, three paths
Page through results. The REST API's query endpoint returns up to 2,000 records per page along with a nextRecordsUrl for the next page. Pagination keeps each request short. The total wall-clock time across all pages may be longer, but no single request runs past the cap:
def fetch_opportunities(session):
url = f"{session['instance_url']}/services/data/v60.0/query"
soql = (
"SELECT Id, Name, Amount, StageName, CloseDate, AccountId "
"FROM Opportunity "
"WHERE LastModifiedDate >= YESTERDAY "
"ORDER BY LastModifiedDate ASC"
)
results = []
response = requests.get(url, params={'q': soql}, headers=session['headers'], timeout=120)
payload = response.json()
results.extend(payload['records'])
while 'nextRecordsUrl' in payload:
next_url = f"{session['instance_url']}{payload['nextRecordsUrl']}"
response = requests.get(next_url, headers=session['headers'], timeout=120)
payload = response.json()
results.extend(payload['records'])
return results
The pages flow one at a time. Each request finishes within seconds. The total fetch time is bounded by the data volume, not by a single connection's duration.
Move to the Bulk API for large volumes. When the data is genuinely large (hundreds of thousands of records or more), the synchronous query endpoint is the wrong tool. Bulk API V2 accepts a query, runs it asynchronously, and returns a job id. The client polls the job until it is complete, then downloads the results:
def bulk_query(session, soql):
create_url = f"{session['instance_url']}/services/data/v60.0/jobs/query"
job = requests.post(create_url, json={
'operation': 'query',
'query': soql
}, headers=session['headers']).json()
job_id = job['id']
while True:
status_url = f"{session['instance_url']}/services/data/v60.0/jobs/query/{job_id}"
status = requests.get(status_url, headers=session['headers']).json()
if status['state'] == 'JobComplete':
break
if status['state'] in ('Failed', 'Aborted'):
raise Exception(f"Bulk job {job_id} ended: {status['state']}")
time.sleep(10)
results_url = f"{session['instance_url']}/services/data/v60.0/jobs/query/{job_id}/results"
return requests.get(results_url, headers=session['headers']).text
The Bulk API has its own throughput characteristics (compressed transport, server-side processing, optimized for large volumes) and avoids the synchronous cap entirely.
Make the query faster. Sometimes the data is not that large but the query is slow. The fix is to optimize the SOQL: add a selective filter on an indexed field, remove subqueries that pull large child sets, project only the fields the consumer needs:
soql = """
SELECT Id, Name, Amount, StageName, CloseDate, AccountId, LastModifiedDate
FROM Opportunity
WHERE LastModifiedDate >= YESTERDAY
AND IsClosed = false
ORDER BY LastModifiedDate ASC
"""
A trimmed projection and a stronger filter cut both the work the platform does and the bytes transferred. The same query that was timing out may now complete in seconds.
For child data, query each child object separately with a filter that joins back to the parent ids. The total request count goes up but each request stays well under the cap.
The fixed example
The middleware rewritten for pagination and async fallback:
def daily_opportunity_sync(session):
soql = (
"SELECT Id, Name, Amount, StageName, CloseDate, AccountId, LastModifiedDate "
"FROM Opportunity "
"WHERE LastModifiedDate >= YESTERDAY "
"ORDER BY LastModifiedDate ASC"
)
try:
records = fetch_paginated(session, soql)
except requests.exceptions.Timeout:
records = bulk_query_and_parse(session, soql)
related_tasks = fetch_related_tasks(session, [r['Id'] for r in records])
related_contacts = fetch_related_contacts(session, [r['Id'] for r in records])
merged = merge_relations(records, related_tasks, related_contacts)
push_to_warehouse(merged)
The primary path paginates the synchronous query. If the request times out (because the volume is unusually large that day), the fallback uses the Bulk API. Related data is fetched in separate, smaller requests.
Edge case: callouts from Apex
Apex callouts have their own timeout limit (120 seconds maximum). A callout that takes 119 seconds also burns CPU time on the Apex side. The callout itself is one tail risk; the Apex CPU time and heap usage during result parsing are another. Async patterns (queueable Apex with future callouts) split the work across multiple transactions:
public class SyncCallout implements Queueable, Database.AllowsCallouts {
public void execute(QueueableContext qc) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Warehouse/sync');
req.setMethod('POST');
req.setTimeout(120000);
req.setBody(buildPayload());
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() != 200) {
// schedule retry
}
}
}
The queueable pattern lets the request continue without holding the user's session open.
Edge case: report runs from the analytics API
A report that runs through the Analytics API has its own timeout. A report with many filter groups, formula columns, and cross-block joins can take longer than the cap. The fix is to push the heavy work into a Scheduled Report and let the platform run it asynchronously, then read the result via the Reports API after completion.
Edge case: long-running approval processes
A flow or approval chain that calls outbound endpoints in sequence can accumulate time. Each step is fast individually but the chain exceeds the cap. Splitting the chain into separate transactions (using flow paused states or via platform events) avoids the cumulative timeout.
Edge case: composite REST requests
The Composite REST API lets a client submit multiple subrequests in a single HTTP call. The composite request has its own time cap; the sum of subrequests must complete within it. A composite that bundles a dozen DML operations and several queries can push past the cap when one subrequest is slow. Splitting the composite into smaller batches keeps each call quick.
The All-or-None flag on a composite request controls whether a single subrequest failure aborts the rest. When time is a concern, splitting into multiple smaller composites (each with All-or-None set appropriately) gives more control over the total wall-clock budget.
Edge case: large file uploads via Chatter Files
Uploading a large binary through the Files REST endpoint counts the upload bytes toward the request timeout. A 200 MB upload over a slow connection can stall past the cap. The Chunked Upload pattern (using ContentVersion with VersionData populated incrementally) splits the upload into smaller pieces and avoids the cap.
Edge case: outbound messages from workflow
A workflow's outbound message sends an XML envelope to an external endpoint. The endpoint must respond within the platform's timeout. A slow endpoint that takes 90 seconds to respond is at risk; one that takes 130 seconds will fail. The standard pattern is to make the external endpoint return quickly (a 200 OK with no body) and have the endpoint enqueue the work on its own side rather than processing the message synchronously.
Defensive habits
Right-size your API choice for the data volume. Synchronous query for tens of records, paginated query for thousands, Bulk API for hundreds of thousands or more.
Always project only the fields the consumer needs. Wide queries with twenty fields take much longer than focused queries with five fields, even when the row count is identical.
Add server-side filters on indexed columns. LastModifiedDate is indexed, as are Id, Name, and most lookup relationships. Filters on these run fast; filters on non-indexed long text are slow.
Monitor query execution times. The Salesforce Developer Console's Query Plan tool reveals the planner's strategy and warns when a query is non-selective. A regression from a 2-second to a 60-second query is a strong leading indicator that the cap is coming.
Implement client-side retry with backoff. A timeout that fires intermittently because of platform load can be retried successfully. A persistent timeout signals a real problem.
Test patterns
Apex-side tests cannot directly trigger the REQUEST_RUNNING_TOO_LONG condition because tests run with synthetic data. The closest test verifies that the pagination and Bulk API fallback paths work correctly:
@IsTest
static void calloutHandlesTimeoutGracefully() {
Test.setMock(HttpCalloutMock.class, new TimeoutMock());
Test.startTest();
try {
System.enqueueJob(new SyncCallout());
} catch (Exception e) {
// expected
}
Test.stopTest();
// assert retry was scheduled or status was marked
}
private class TimeoutMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
throw new CalloutException('Read timed out');
}
}
The mock simulates a timeout and confirms the handler's recovery path runs.
Diagnosing in production
When the error fires:
- Capture the request that timed out (URL, parameters, payload).
- Estimate the volume: how many records would the query return on a normal day?
- Run the same query in the Developer Console's Query Plan to see the cost.
- Decide: split the query, add filters, paginate, or move to Bulk API.
- Deploy the fix and rerun the integration.
For one-off recovery, run a smaller scope (one day at a time, one region at a time) until the data is caught up. The structural fix lands in the next release.
Anti-pattern: cranking up the client timeout
A reflex is to set the client's HTTP timeout to 600 seconds and hope the platform doesn't enforce its cap. The platform's cap is independent of the client's timeout. A 600-second client waiting on a 120-second server still gets the REQUEST_RUNNING_TOO_LONG response after 120 seconds.
Anti-pattern: ignoring intermittent timeouts
A timeout that fires once a week is sometimes treated as "Salesforce being flaky." This dismisses a real signal. Intermittent timeouts usually mean the query is on the edge of the cap and slight data volume changes push it over. The fix should happen before the timeout becomes daily.
Quick recovery checklist
- Identify the request that timed out.
- Estimate the data volume.
- Pick the right API pattern (paginated query, Bulk API, async Apex).
- Add filters or trim projections.
- Deploy and confirm.
The error class is common in growing integrations. Once the team has internalized the right pattern, new integrations get the right shape from the start.
Further reading from Salesforce
- REST API Developer Guide: Query
- Bulk API 2.0 Developer Guide: Query
- Apex Developer Guide: Callouts and Timeouts
- Architect: API Integration Patterns and Practices
- Trailhead: API Basics
Related dictionary terms
Share this fix
Related Integration errors
API_DISABLED_FOR_ORG: API is not enabled for this Organization or Partner
IntegrationThe org's edition or the user's profile doesn't include API access. Most often this is a Professional Edition org without the API add-on, or…
ChangeDataCapture: missing ChangeEventHeader / cannot replay events
IntegrationYour Change Data Capture subscriber isn't receiving events, or events are arriving without a `ChangeEventHeader`. Usually means CDC isn't en…
DUPLICATES_DETECTED: Use one of these records?
IntegrationSalesforce's Duplicate Rules engine flagged your record as a fuzzy match for an existing one. Different from `DUPLICATE_VALUE`, which is a h…
EXCEEDED_ID_LIMIT: record limit reached
IntegrationYou hit one of Salesforce's hard caps on a record-related operation: too many sharing rows on one record, too many child records on one pare…
Failed to load batch — InvalidBatch: invalid CSV header / unrecognized field
IntegrationYour Bulk API job's CSV has a column the target object doesn't have, or the header row is malformed. The job's `failedResults` lists the bad…