This error occurs when attempting to reference a savepoint that does not exist, typically because it was never created, already released, or the parent transaction has ended.
The PostgreSQL error code 3B001 (invalid_savepoint_specification) indicates that your code attempted to reference a savepoint that cannot be found in the current transaction. Savepoints are named markers within a transaction that allow you to roll back to a specific point without aborting the entire transaction. This error most commonly occurs when using ROLLBACK TO SAVEPOINT or RELEASE SAVEPOINT commands with a savepoint name that either was never created with SAVEPOINT, has already been released or rolled back, or existed in a transaction that has since committed or rolled back. The error is frequently encountered when using ORMs (like Ecto, Doctrine, Spring Data JPA) that manage savepoints automatically for nested transactions. PostgreSQL maintains savepoint state strictly within transaction boundaries, so once a transaction ends (via COMMIT or ROLLBACK), all its savepoints are destroyed. Similarly, rolling back to a savepoint destroys all savepoints created after it.
Check your transaction logic to ensure the savepoint is created before attempting to use it:
-- Correct pattern
BEGIN;
SAVEPOINT my_savepoint;
-- ... operations ...
ROLLBACK TO SAVEPOINT my_savepoint; -- OK
COMMIT;
-- Incorrect pattern
BEGIN;
-- Missing SAVEPOINT command here
ROLLBACK TO SAVEPOINT my_savepoint; -- ERROR: no such savepoint
COMMIT;Ensure every ROLLBACK TO SAVEPOINT or RELEASE SAVEPOINT has a corresponding SAVEPOINT command earlier in the same transaction.
Verify that savepoint operations occur within the same transaction block:
-- Wrong: savepoint used after transaction ended
BEGIN;
SAVEPOINT sp1;
COMMIT; -- Transaction ends, sp1 is destroyed
ROLLBACK TO SAVEPOINT sp1; -- ERROR: no such savepoint
-- Correct: savepoint used within same transaction
BEGIN;
SAVEPOINT sp1;
-- ... operations ...
ROLLBACK TO SAVEPOINT sp1; -- OK
COMMIT;If you see this error at transaction boundaries, restructure your code to keep savepoint operations within a single transaction.
Check for code that releases the same savepoint multiple times:
-- Wrong: releasing same savepoint twice
BEGIN;
SAVEPOINT sp1;
RELEASE SAVEPOINT sp1; -- OK
RELEASE SAVEPOINT sp1; -- ERROR: no such savepoint
-- Correct: release only once
BEGIN;
SAVEPOINT sp1;
RELEASE SAVEPOINT sp1; -- OK
-- Don't reference sp1 again
COMMIT;Use conditional logic or tracking variables to ensure savepoints are only released once.
If using an ORM, review how it manages savepoints for nested transactions:
Ecto (Elixir):
# Check for proper transaction nesting
Repo.transaction(fn ->
# Outer transaction
Repo.transaction(fn ->
# Inner transaction creates savepoint
# Ensure both complete successfully
end)
end)Doctrine (PHP):
// Verify savepoint nesting matches transaction nesting
$em->getConnection()->beginTransaction();
$em->getConnection()->createSavepoint('DOCTRINE2_SAVEPOINT_1');
// ... operations ...
$em->getConnection()->releaseSavepoint('DOCTRINE2_SAVEPOINT_1');
$em->getConnection()->commit();Enable SQL logging to see the exact savepoint commands being issued by your ORM.
Understand that rolling back to a savepoint destroys later savepoints:
BEGIN;
SAVEPOINT sp1;
-- ... operations ...
SAVEPOINT sp2;
-- ... operations ...
ROLLBACK TO SAVEPOINT sp1; -- sp2 is now destroyed
ROLLBACK TO SAVEPOINT sp2; -- ERROR: no such savepoint
-- Correct approach
BEGIN;
SAVEPOINT sp1;
SAVEPOINT sp2;
ROLLBACK TO SAVEPOINT sp1; -- sp2 destroyed
SAVEPOINT sp2; -- Create sp2 again if needed
COMMIT;After rolling back to an earlier savepoint, recreate any later savepoints if needed.
Implement savepoint tracking in your application layer:
# Python example with psycopg2
class SavepointManager:
def __init__(self):
self.active_savepoints = set()
def create(self, conn, name):
conn.execute(f"SAVEPOINT {name}")
self.active_savepoints.add(name)
def rollback_to(self, conn, name):
if name not in self.active_savepoints:
raise ValueError(f"Savepoint {name} does not exist")
conn.execute(f"ROLLBACK TO SAVEPOINT {name}")
# Remove this and all later savepoints
self.active_savepoints = {sp for sp in self.active_savepoints
if sp <= name}
def release(self, conn, name):
if name not in self.active_savepoints:
raise ValueError(f"Savepoint {name} does not exist")
conn.execute(f"RELEASE SAVEPOINT {name}")
self.active_savepoints.remove(name)This prevents invalid savepoint operations before they reach the database.
Savepoint Lifecycle and Implicit Destruction:
When you ROLLBACK TO SAVEPOINT, PostgreSQL implicitly destroys all savepoints established after the named savepoint. This cascading behavior is often overlooked and can cause unexpected 3B001 errors if your code assumes those later savepoints still exist.
ORM Nested Transaction Patterns:
Many ORMs implement nested transactions using savepoints. For example:
- First begin_transaction() issues BEGIN
- Second begin_transaction() issues SAVEPOINT sp_1
- Third begin_transaction() issues SAVEPOINT sp_2
If an ORM's state management gets out of sync with the actual database state (often due to exception handling or commit/rollback logic bugs), it may attempt to release or rollback to savepoints that no longer exist. Review your ORM's issue tracker for known savepoint-related bugs.
PL/pgSQL Considerations:
You cannot use SAVEPOINT, ROLLBACK TO SAVEPOINT, or RELEASE SAVEPOINT directly in PL/pgSQL functions. Instead, use BEGIN...EXCEPTION...END blocks, which create implicit savepoints. Attempting to use explicit savepoint commands in PL/pgSQL will raise "ERROR: unsupported transaction command in PL/pgSQL".
Cursors and Savepoints:
A cursor whose execution causes a transaction to abort is put in a cannot-execute state. While the transaction can be restored using ROLLBACK TO SAVEPOINT, the cursor cannot be used after the rollback. You must close and reopen the cursor after a savepoint rollback.
Testing in Development:
Enable full SQL logging (log_statement = 'all') to see the exact sequence of SAVEPOINT, ROLLBACK TO SAVEPOINT, and RELEASE SAVEPOINT commands. This makes it easier to identify where the savepoint lifecycle breaks down.
ERROR: syntax error at end of input
Syntax error at end of input in PostgreSQL
Bind message supplies N parameters but prepared statement requires M
Bind message supplies N parameters but prepared statement requires M in PostgreSQL
Multidimensional arrays must have sub-arrays with matching dimensions
Multidimensional arrays must have sub-arrays with matching dimensions
ERROR: value too long for type character varying
Value too long for type character varying
insufficient columns in unique constraint for partition key
How to fix "insufficient columns in unique constraint for partition key" in PostgreSQL