An integration log shows HTTP 200 for every Salesforce Composite API call, yet half the records never saved. That bug is common, and it usually comes from treating four different resources as one. Composite, Composite Batch, Composite Graph and sObject Collections all reduce round trips. They behave very differently when it comes to how subrequests relate, what rolls back on failure, and how each call counts against your API allocation.
These resources are one option among several for inbound work; choosing a Salesforce integration pattern shows where they fit. If the REST API itself is new to you, the integration patterns overview has the basics.
The four resources at a glance
| Resource | Subrequest limit | Can reference earlier results? | Failure behaviour | Counts against daily API allocation |
|---|---|---|---|---|
| Composite | 25 (max 5 queries or sObject Collections) | Yes, with @{referenceId} | Set allOrNone explicitly: true rolls the whole request back on any failure | Once for the whole request |
| Composite Batch | 25 | No | Each subrequest commits on its own; earlier work stays | Each subrequest counts |
| Composite Graph | 500 nodes per graph | Yes, within a graph | Each graph saves or rolls back as a unit | Once for the whole request |
| sObject Collections | 200 records, one operation | Not applicable | allOrNone flag decides | Once per call |
Composite: dependent steps in one round trip
Composite runs subrequests in order, and a later subrequest can use the output of an earlier one. The classic example creates an Account and then a Contact that points at it:
{
"allOrNone": true,
"compositeRequest": [
{
"method": "POST",
"url": "/services/data/v67.0/sobjects/Account",
"referenceId": "newAccount",
"body": { "Name": "Acme Ltd" }
},
{
"method": "POST",
"url": "/services/data/v67.0/sobjects/Contact",
"referenceId": "newContact",
"body": { "LastName": "Rao", "AccountId": "@{newAccount.id}" }
}
]
}
Things to remember:
- The whole request counts as one API call. That is the main reason to use it.
- Up to 25 subrequests, and no more than 5 of them can be queries or sObject Collections operations.
- Set
allOrNoneon every request. Withtruethe whole Composite request rolls back when any subrequest fails; withfalsethe independent subrequests still save and only the dependants of the failed one are skipped. The docs mark the flag optional without stating a default, so never rely on one. Plain REST calls behave the other way round: each record stands alone unless you ask for all or nothing. - Subrequests still share one transaction’s governor limits. Bundling them saves API calls. It does nothing for CPU time, SOQL counts or DML limits.
Composite Batch: unrelated calls bundled together
Composite Batch also takes up to 25 subrequests, but they are independent. You can’t pass data between them, and although they run in the order you list them, each one commits separately. If the third subrequest fails, the first two stay committed, and the batch keeps going unless you set haltOnError. A batch that runs longer than 10 minutes times out and the remaining subrequests are skipped.
It is also a poor bulk tool. Each subrequest counts against rate limits, so bundling saves round trips without saving any allocation.
Use it when you have a handful of unrelated reads or writes and want one network trip instead of several.
Composite Graph: parents and children, saved together
Composite Graph lets you describe a set of related records as a graph. Salesforce works out the order (parents before children) and treats each graph as a single unit: it saves completely or rolls back completely. Several graphs can travel in one payload, and a failure in one graph doesn’t affect the others.
That makes it the natural choice for “create this order with its line items” when you are sending many orders at once.
Composite Graph limits
| Limit | Value |
|---|---|
| Graphs per payload | 75 |
| Maximum depth of a graph | 15 |
| Nodes per graph | 500 |
| Nodes per payload, across all graphs | 500 |
| Distinct node types per payload | 15 |
| Graph failures before the request stops | 14 |
sObject Collections: straightforward bulk CRUD
sObject Collections handles up to 200 records of the same operation in one call. There is no job to create or poll, so it sits neatly between single-record REST and Bulk API 2.0. Above 200 records, move to Bulk API 2.0; the data migration strategy guide covers large loads.
The HTTP 200 trap
A Composite request returns HTTP 200 even when a subrequest failed and the whole request rolled back. The top-level status only tells you the Composite call was processed. The real outcome of each step is in the response body.
{
"compositeResponse": [
{
"referenceId": "newAccount",
"httpStatusCode": 400,
"body": [{ "errorCode": "PROCESSING_HALTED", "message": "..." }]
}
]
}
A client that checks only the status code will log success for work that never saved. This shows up in production far more often than it should. Always loop through compositeResponse and check each httpStatusCode.
Picking one
| Situation | Use |
|---|---|
| Create an Account, then a Contact that references it | Composite |
| Three unrelated lookups in one network trip | Composite Batch |
| Hundreds of orders, each with line items, each order independent | Composite Graph |
| 150 Contact updates with no dependencies | sObject Collections |
| 50,000 records | Bulk API 2.0 |
Comparing them out loud
If you’re asked to compare them, lead with the relationship between subrequests: Composite subrequests can reference each other, Batch subrequests are independent, and Graph organises records into units that save or fail together. Then mention API call counting and the HTTP 200 behaviour. Those two details signal that you have used the API against a real org.