skip to content

Why might a @Transactional(timeout = ...) value appear to be ignored at runtime? Cover propagation and what the timeout does and doesn't bound.

level: seniorimportance: should knowfreq 28%

answer

  1. Timeout binds only to a newly-begun tx
  2. REQUIRED join -> inner timeout ignored
  3. validateExistingTransaction false=silent, true=throws
  4. REQUIRES_NEW for own budget
  5. Boundary-checked; CPU/sleep not interrupted

basics

~20 s

The timeout only applies to the transaction Spring actually starts. If your method joins an existing (PROPAGATION_REQUIRED) transaction, your timeout is silently ignored by default. Also, timeout is enforced only at database-statement boundaries, so pure CPU/sleep work between queries isn't interrupted mid-flight.

solid answer

~50 s

Two big reasons. First, propagation: a timeout is set only when Spring **begins** a new physical transaction. With the default PROPAGATION_REQUIRED, an inner @Transactional method that joins the caller's existing transaction does not get a fresh timeout — the outer transaction's timeout governs, and the inner value is ignored (Spring only flags this mismatch if validateExistingTransaction is enabled, which is off by default). To guarantee your timeout takes effect, the method must start a new transaction (be the outermost, or use REQUIRES_NEW). Second, what it bounds: enforcement happens at statement boundaries via Statement.setQueryTimeout and the deadline check. Long non-DB work — a CPU loop, an external HTTP call, Thread.sleep — between queries won't be aborted while it runs; the timeout is only noticed when the next database operation checks the deadline. And if the JDBC driver doesn't honor setQueryTimeout, even in-flight queries won't be cancelled.

code

java · 15 lines
java
@Service
public class OrderService {

    @Transactional(timeout = 30)          // starts the physical tx: 30s wins
    public void process() {
        auditQuickly();                    // timeout=2 IGNORED (joins this tx)
        chargeInSeparateTx();              // REQUIRES_NEW: own 2s deadline
    }

    @Transactional(timeout = 2)            // PROPAGATION_REQUIRED (default): joins -> ignored
    public void auditQuickly() { /* ... */ }

    @Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 2)
    public void chargeInSeparateTx() { /* suspends outer, new 2s tx */ }
}

go deeper

for a junior

Know that a nested @Transactional often joins the outer one, so its timeout may not apply.

for a middle

Explain PROPAGATION_REQUIRED join vs REQUIRES_NEW and that timeout binds to a newly-started tx.

for a senior

Add validateExistingTransaction semantics, the statement-boundary enforcement limitation, and self-invocation.

for a principal

Turn it into a policy: keep non-DB work out of transactions, own the timeout at the physical boundary, and add DB-side timeouts for defense in depth.

## Reason 1: propagation — the timeout binds to a *new* transaction only Spring assigns a timeout deadline only in `doBegin`, i.e. when it **physically starts** a transaction. Consider: ```java @Transactional(timeout = 30) public void outer() { inner(); } @Transactional(timeout = 2) // PROPAGATION_REQUIRED (default) public void inner() { ... } ``` `inner()` **joins** `outer()`'s transaction rather than starting a new one, so its `timeout = 2` is **not applied**. The already-running transaction keeps its 30-second deadline. By default `AbstractPlatformTransactionManager.validateExistingTransaction` is `false`, so Spring **silently ignores** the mismatch. If you set that flag to `true`, Spring instead **throws** `IllegalTransactionStateException` complaining the participating definition's timeout differs from the existing one. Either way, the inner value never quietly 'wins.' To make a tighter timeout actually take effect on a nested call, use `@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 2)` — that **suspends** the outer transaction and begins a new one with its own 2-second deadline. Also note: self-invocation (a method in the same bean calling another `@Transactional` method via `this`) bypasses the proxy entirely, so the second annotation — including its timeout — is ignored. That's a separate but commonly-conflated reason a timeout 'does nothing.' ## Reason 2: it bounds DB work, checked at statement boundaries The timeout is enforced by (a) `Statement.setQueryTimeout(remaining)` on each JDBC statement and (b) a deadline check via `getTimeToLiveInSeconds()` before Spring hands out that remaining time. Implications: - A **single long query** is cancelled by the database (query timeout). - **Non-DB work between queries** — a busy loop, `Thread.sleep`, a slow REST call — is **not interrupted** while it runs. The overshoot is only detected when the *next* database statement runs and finds the deadline passed, at which point `TransactionTimedOutException` is thrown. So a method that does one query then computes for 10 minutes with `timeout = 5` will not be aborted at 5s; it finishes the CPU work and only fails if it touches the DB again. - If the **driver ignores `setQueryTimeout`** (not all do, and behavior varies), even in-flight queries won't be cancelled and you rely solely on the boundary check. ## Reason 3: value validity / not being in a transaction at all - If the method isn't actually transactional (proxy not applied, `@Transactional` on a non-public method with the default proxy config, or self-invocation), the timeout is irrelevant. - Some transaction managers (`JtaTransactionManager` in certain setups) may not support a per-transaction timeout the same way; the value can be delegated to the JTA provider (`UserTransaction.setTransactionTimeout`). ## Practical guidance - Put the timeout on the **outermost** transactional boundary that owns the physical transaction, or use `REQUIRES_NEW` when a nested unit genuinely needs its own budget. - Don't expect a timeout to protect against slow **non-database** operations — keep external calls out of transactions, or guard them with their own timeouts. - Pair the Spring timeout with DB-side `statement_timeout` / `lock_timeout` for defense in depth, since Spring's mechanism depends on driver cooperation.

  • How do you make a nested method's tighter timeout actually take effect?
    Use Propagation.REQUIRES_NEW so Spring suspends the outer transaction and begins a new physical transaction with its own timeout/deadline.
  • What does validateExistingTransaction control here?
    It's a flag on AbstractPlatformTransactionManager (default false). When true, joining a transaction with a mismatched timeout (or read-only) throws IllegalTransactionStateException instead of silently ignoring the inner value.
  • Will timeout = 5 abort a method that runs a 10-minute CPU loop after one query?
    No. The deadline is only checked at DB-statement boundaries. The loop runs to completion; expiry is detected only if/when the next database statement runs, then throws TransactionTimedOutException.

saying these in an interview costs you the question

  • Assuming the inner @Transactional timeout always overrides the outer
  • Thinking timeout interrupts arbitrary CPU or HTTP work mid-execution
  • Not knowing REQUIRES_NEW is the way to give a nested unit its own timeout
  • Confusing self-invocation timeout loss with propagation

context