quarkusio/quarkus · error · RollbackException

Transaction was already rolled back (e.g., by the transactio

Error message

Transaction was already rolled back (e.g., by the transaction reaper)

What it means

At the end of a @Transactional method, endTransaction commits/rolls back the transaction, but first it verifies a transaction still exists on the thread. If tm.getTransaction() returns null, the transaction vanished — typically because the Narayana transaction reaper aborted it after the timeout — and this RollbackException is thrown. It signals your transaction exceeded its timeout and work was rolled back behind your back.

Source

Thrown at extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorBase.java:453

                tx.setRollbackOnly();
            }
        } catch (IllegalStateException e) {
            log.warn("Cannot set rollback-only, transaction already ended", e);
        }
    }

    protected void handleException(InvocationContext ic, Throwable t, Transaction tx) throws Exception {

        handleExceptionNoThrow(ic, t, tx);
        sneakyThrow(t);
    }

    protected void endTransaction(TransactionManager tm, Transaction tx, RunnableWithException afterEndTransaction)
            throws Exception {
        try {
            Transaction current = tm.getTransaction();
            if (current == null) {
                throw new RollbackException("Transaction was already rolled back (e.g., by the transaction reaper)");
            }
            if (tx != current) {
                throw new RuntimeException(jtaLogger.i18NLogger.get_wrong_tx_on_thread());
            }

            if (tx.getStatus() == Status.STATUS_MARKED_ROLLBACK) {
                tm.rollback();
            } else {
                tm.commit();
            }
        } finally {
            afterEndTransaction.run();
        }
    }

    protected boolean setUserTransactionAvailable(boolean available) {
        boolean previousUserTransactionAvailability = ServerVMClientUserTransaction.isAvailable();

View on GitHub (pinned to e1c734241f)

Solutions

  1. Increase the transaction timeout with @TransactionConfiguration(timeout=...) on the entry method or quarkus.transaction.default-transaction-timeout globally
  2. Move slow external I/O outside the transaction boundary so the TX window stays short
  3. Tune the reaper (quarkus.transaction.periodic-recovery-period / reaper-related settings) and investigate why the TX ran so long
  4. Handle work loss idempotently — the TX rolled back, so retry the operation once the slowness is fixed

Example fix

// before
@Transactional
void importAll() { slowHttpCall(); ... } // >60s, reaper aborts
// after
@Transactional @TransactionConfiguration(timeout = 600)
void importAll() { ... }
Defensive patterns

Strategy: retry

Validate before calling

int status = transactionManager.getStatus();
boolean txStillAlive = (status == jakarta.transaction.Status.STATUS_ACTIVE);
// if not alive before committing, the reaper already killed it

Try / catch

try { @Transactional work } catch (RollbackException e) {
    if (e.getMessage().contains("already rolled back")) { /* retry idempotently with larger timeout */ }
}

Prevention

When it happens

Trigger: A @Transactional method runs longer than the configured transaction timeout (default 60s); the reaper rolls back and aborts the TX; when the interceptor later calls endTransaction, the thread's transaction is gone, so tm.getTransaction()==null. Also invoked from handleAsync for deferred completion.

Common situations: Slow external calls (HTTP, DB queries) inside a transaction exceeding the timeout; large batch processing in one TX; deadlock/stuck DB connection until timeout; default timeout too small for the workload.

Related errors


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/18410ba32d49d207. Report an issue: GitHub.