Database transaction opened without a deferred rollback

A database transaction is opened and committed in the same function with no `defer tx.Rollback()` guard, so if an error path returns early the transaction is silently left open, holding connections and locks.

missing-transaction-rollback high confidence runtime go

Database transaction opened without a deferred rollback

What this failure means

A database transaction is opened and committed in the same function with no defer tx.Rollback() guard, so if an error path returns early the transaction is silently left open, holding connections and locks.

Diagnosis

A db.Begin() or db.BeginTx() call is followed by tx.Commit() in the same function, but there is no defer tx.Rollback() to release the transaction on error paths.

The idiomatic Go pattern defers a rollback immediately after a successful Begin:

tx, err := db.Begin()
if err != nil { return err }
defer tx.Rollback() // safe no-op if Commit already succeeded

Without the deferred rollback:

  • Any early return on an error before Commit leaves the transaction open until the database connection times out.
  • Under concurrent load, leaked transactions exhaust the connection pool.
  • Table locks held by uncommitted transactions block other writers.

Fix steps

  1. Add defer tx.Rollback() immediately after a successful Begin:
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return fmt.Errorf("begin tx: %w", err)
    }
    defer tx.Rollback() // always deferred; no-op after a successful Commit
    
    // ... mutations ...
    
    return tx.Commit()
    
  2. Return the error from Commit so the caller knows if it failed.
  3. Avoid capturing tx in a closure that may outlive the deferred rollback.

Validation

  • Run faultline inspect . from the repository root and confirm this source finding is absent or intentionally mitigated.
  • Test that an error on any path before Commit releases the connection cleanly without exhausting the pool.

Why it matters

Unreleased transactions hold database connections and table locks. Under load, a handful of leaked transactions can exhaust the connection pool and cause cascading failures across every service that shares the same database.

Try it locally

make test
rg -n 'db.Begin|db.BeginTx' .
make test
go vet ./...

How Faultline detects it

Use faultline explain missing-transaction-rollback to see the full playbook.

faultline analyze build.log
faultline explain missing-transaction-rollback

Generated from playbooks/bundled/source/missing-transaction-rollback.yaml. Do not edit directly.

Try it on your own failed log

$ faultline analyze failed.log
Want this across every CI run? Faultline Teams tracks recurring failures across all your repos and surfaces patterns in a shared dashboard.