Briefly: a savepoint marks a point in the current transaction that you can return to with Database.rollback. Recent API versions cap you at five savepoints and five rollbacks per transaction, and each of those calls consumes one of your 150 DML statements. A rollback undoes database changes and nothing else, which is the part people get wrong: static variables, already dispatched callouts and the ids stamped onto inserted sObjects all survive it.
What a savepoint is actually for
Apex already gives you all or nothing behaviour for free. An uncaught exception rolls the whole transaction back. So a savepoint is only useful when you want to catch a failure, undo part of the work, and then carry on doing something else in the same transaction.
The legitimate cases are narrow:
- A multi step process where step three can fail and steps one and two should not stand on their own.
- Trying an operation, catching the specific failure, and writing an error record instead.
- A batch execute that must leave the chunk clean while letting the job continue.
If you are going to rethrow the exception anyway, you do not need a savepoint. The platform will do the rollback for you and the code is shorter.
The shape
Savepoint sp = Database.setSavepoint();
try {
insert orderHeader;
insert lineItems;
updateInventory(lineItems);
} catch (DmlException e) {
Database.rollback(sp);
ErrorLogger.publish('Order build failed', e);
orderHeader.Id = null; // see below
throw new OrderException('Could not create order', e);
}
Catch a specific exception type, since catching Exception swallows limit exceptions you cannot recover from. And log through something that survives the rollback, which means a platform event published immediately, not an insert.
What a rollback does not undo
Static variables. Any flag, counter or accumulated collection you set before the rollback keeps its value. A recursion guard that recorded “already processed these ids” will still claim they were processed, so the retry does nothing. If your rollback path needs to run the same logic again, reset the statics by hand.
Ids on inserted sObjects. This is the sharp edge. After an insert, the in memory record has an Id. Rolling back removes the row from the database but leaves the Id populated on your Apex object. Insert it again and you get INVALID_FIELD_FOR_INSERT_UPDATE, because the platform sees a record that already has an Id. Null the Id before reusing the instance, or rebuild it from scratch.
Callouts. An HTTP request you already sent has been received. There is no compensating action unless the other system offers one, which is the main argument for doing callouts before any DML rather than in the middle of it.
Limit consumption. The queries, DML statements, rows and CPU you spent before the rollback are spent. A rollback restores data, not budget.
The counting rules
Both Database.setSavepoint() and Database.rollback() count as DML statements against the limit of 150. That makes a savepoint inside a loop over 200 records an immediate failure, and it is the most common way people meet this feature.
Savepoints also invalidate each other. If you set savepoint A, then savepoint B, then roll back to A, savepoint B is no longer usable. Rolling back to a savepoint discards every savepoint created after it. Nesting them in a way that depends on the later one still being valid will fail at runtime rather than at compile time.
The cap of five in each direction is generous for a design that needs savepoints and restrictive for one using them as general control flow. Treat hitting it as a signal to restructure.
When to use one, and when not to
Use a savepoint when a failure must be handled inside the transaction and partly completed data would be wrong. A common example is an integration endpoint that has to return a structured error body rather than a raw exception, while leaving no half written records behind.
Do not use one to work around a trigger that fires something you did not want, to implement per record error handling in a bulk load, or anywhere inside a loop. For per record handling, Database.insert(records, false) gives you partial success without any savepoint at all, and it costs one DML statement rather than three.
Verified in a Summer ’26 scratch org on 14 September 2026: after Database.setSavepoint() and an insert, a count query returned 1. After Database.rollback(sp) the same query returned 0. The rollback undid the DML inside the same transaction.
Frequently asked
Does a rollback undo work done by triggers and flows? Yes, as far as the database is concerned. Anything those automations wrote after the savepoint is removed. Anything they sent outside the platform is not.
Can I roll back to the same savepoint twice? Yes, while it remains valid, and both rollbacks count against the five rollback cap and against your DML statements.
Do savepoints work across asynchronous boundaries? No. A queueable or future method runs in its own transaction, so a rollback in the caller has no effect on work already enqueued and executed separately. This is also why enqueuing a job for logging does not survive a failure: the enqueue itself is rolled back.
