plandex-ai/plandex · error
error committing transaction: %v
Error message
error committing transaction: %v
What it means
After inserting the lock row inside a REPEATABLE READ transaction, lockRepoDB commits with tx.Commit(); if the commit fails it returns 'error committing transaction'. The lock row is rolled back, so no lock was acquired even though the INSERT succeeded. The underlying driver error is wrapped in the message.
Source
Thrown at app/server/db/locks.go:376
if insertedId.Valid {
newLock.Id = insertedId.String
} else {
if locksVerboseLogging {
log.Printf("no rows returned from insert query, means there was a conflict")
}
return retryWithExponentialBackoff(params.Ctx, err, numRetry, func(nextAttempt int) (string, error) {
return lockRepoDB(params, nextAttempt)
})
}
if locksVerboseLogging {
log.Printf("[Lock][%d] INSERT took %v | reason: %s",
goroutineID, time.Since(insertStart), params.Reason)
}
// Commit the transaction
if err = tx.Commit(); err != nil {
return "", fmt.Errorf("error committing transaction: %v", err)
}
committed = true
activeLockIdsMu.Lock()
activeLockIds[newLock.Id] = true
activeLockIdsMu.Unlock()
log.Printf("Lock acquired: %s for plan %s with scope %s | reason: %s", newLock.Id, planId, scope, params.Reason)
// Start a goroutine to keep the lock alive
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in heartbeat goroutine: %v\n%s", r, debug.Stack())
cancelFn()
go notify.NotifyErr(notify.SeverityError, fmt.Errorf("panic in lock heartbeat goroutine: %v\n%s", r, debug.Stack()))
}View on GitHub (pinned to e2d772072e)
Solutions
- Check the wrapped cause; a 40001 serialization error means the transaction should be retried from the beginning
- Keep the lock transaction short — do nothing slow between BEGIN and COMMIT
- Review proxy/pooler (PgBouncer/Haproxy) idle timeouts versus transaction duration
- Confirm Postgres server logs for the matching backend error (connection reset, termination)
- Retry the whole lock acquisition (lockRepoDB starts a fresh transaction each attempt)
Example fix
// before
id, err := lockRepoDB(ctx, planId, orgId, scope)
// after
id, err := lockRepoDB(ctx, planId, orgId, scope)
if err != nil && strings.Contains(err.Error(), "error committing transaction") {
// lock was NOT acquired; safe to retry entire acquisition after backoff
time.Sleep(time.Second)
id, err = lockRepoDB(ctx, planId, orgId, scope)
} Defensive patterns
Strategy: retry
Validate before calling
if Conn == nil {
return errors.New("db connection not initialized")
}
if err := Conn.PingContext(ctx); err != nil {
return fmt.Errorf("db unreachable: %w", err)
} Type guard
func isCommitFailure(err error) bool {
return err != nil && strings.Contains(err.Error(), "error committing transaction")
} Try / catch
id, err := lockRepoDB(ctx, orgId, planId, reason)
if isCommitFailure(err) {
// lock NOT acquired; whole transaction rolled back — retry from scratch
time.Sleep(time.Second)
id, err = lockRepoDB(ctx, orgId, planId, reason)
} Prevention
- Keep the lock transaction short — no slow work between BEGIN and COMMIT
- Tune pooler/proxy idle timeouts above worst-case transaction duration
- Retry the full acquisition on commit failure; never assume partial lock
- Watch Postgres logs for commit-phase 40001 errors
When it happens
Trigger: tx.Commit() returns an error: network drop between INSERT and COMMIT, Postgres terminating the backend, serialization failure at commit under REPEATABLE READ (40001), or commit canceled because the connection's context expired.
Common situations: Long-running transaction crossing a failover or load-balancer idle timeout; commit race with another lock holder causing serialization abort; DB connection closed by PgBouncer after idle_timeout.
Related errors
- error deleting custom models: %v
- error deleting custom providers: %v
- error adding org user: %v
- error storing default plan config: %v
- error getting default plan config: %v
AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05).
Data as JSON: /api/errors/f2c494a6b455e625.
Report an issue: GitHub.