{"record":{"id":"9a76010b8aa02232","repo":"quarkusio/quarkus","slug":"changing-timeout-via-transactionconfiguration-can","errorCode":null,"errorMessage":"Changing timeout via @TransactionConfiguration can only be done at the entry level of a transaction","messagePattern":"Changing timeout via @TransactionConfiguration can only be done at the entry level of a transaction","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorBase.java","lineNumber":383,"sourceCode":"            checkConfiguration(ic);\n            return ic.proceed();\n        } catch (Throwable t) {\n            handleException(ic, t, tx);\n        }\n        throw new RuntimeException(\"UNREACHABLE\");\n    }\n\n    protected Object invokeInNoTx(InvocationContext ic) throws Exception {\n        checkConfiguration(ic);\n        return ic.proceed();\n    }\n\n    private void checkConfiguration(InvocationContext ic) {\n        TransactionConfiguration configAnnotation = getTransactionConfiguration(ic);\n        if (configAnnotation != null && ((configAnnotation.timeout() != TransactionConfiguration.UNSET_TIMEOUT)\n                || !TransactionConfiguration.UNSET_TIMEOUT_CONFIG_PROPERTY\n                        .equals(configAnnotation.timeoutFromConfigProperty()))) {\n            throw new RuntimeException(\"Changing timeout via @TransactionConfiguration can only be done \" +\n                    \"at the entry level of a transaction\");\n        }\n    }\n\n    protected void handleExceptionNoThrow(InvocationContext ic, Throwable t, Transaction tx)\n            throws IllegalStateException, SystemException {\n        Transactional transactional = getTransactional(ic);\n\n        for (Class<?> dontRollbackOnClass : transactional.dontRollbackOn()) {\n            if (dontRollbackOnClass.isAssignableFrom(t.getClass())) {\n                return;\n            }\n        }\n\n        for (Class<?> rollbackOnClass : transactional.rollbackOn()) {\n            if (rollbackOnClass.isAssignableFrom(t.getClass())) {\n                safeSetRollbackOnly(tx);\n                return;","sourceCodeStart":365,"sourceCodeEnd":401,"githubUrl":"https://github.com/quarkusio/quarkus/blob/e1c734241f34c7919086ceb4c9262b4a58f6de44/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorBase.java#L365-L401","documentation":"@TransactionConfiguration(timeout=...) sets the JTA transaction timeout, which is only meaningful when the transaction is first started. If the interceptor sees this annotation on a method that merely joins an existing transaction (invoked in caller's TX or in an ongoing TX), it throws this RuntimeException because the timeout would silently be ignored. The check runs in checkConfiguration, invoked from invokeInCallerTx and invokeInNoTx.","triggerScenarios":"Nested call chain: outer @Transactional method starts the TX, an inner @Transactional method also declares @TransactionConfiguration(timeout=X or timeoutFromConfigProperty) and is invoked while a transaction is already active (REQUIRED joins the existing TX).","commonSituations":"Developers adding @TransactionConfiguration to a helper/service method assuming it applies per-method, not realizing an outer method already opened the transaction; copy-pasting the timeout annotation across a call graph; changing MANDATORY methods to add timeouts.","solutions":["Move @TransactionConfiguration(timeout) to the outermost/entry method that actually starts the transaction","Remove the timeout annotation from inner joining methods (it has no effect there anyway)","Change the inner method's @Transactional(TxType.REQUIRES_NEW) if it truly needs its own timeout — that makes it an entry point","Set the default timeout globally via quarkus.transaction.default-transaction-timeout if all transactions need more time"],"exampleFix":"// before\n@Transactional @TransactionConfiguration(timeout = 120)\npublic void inner() { ... }\n// after: put config on entry point\n@Transactional @TransactionConfiguration(timeout = 120)\npublic void outer() { inner(); }\n@Transactional\npublic void inner() { ... }","handlingStrategy":"validation","validationCode":"// only annotate the entry method that starts the TX\nboolean isEntryMethod = transactionManager.getStatus() == jakarta.transaction.Status.STATUS_NO_TRANSACTION;\n// apply @TransactionConfiguration only when isEntryMethod","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Put @TransactionConfiguration only on methods that START transactions","Avoid @TransactionConfiguration on methods called from other @Transactional methods","Use quarkus.transaction.default-transaction-timeout for global timeout needs","Review nested @Transactional call graphs when adding timeout config"],"tags":["jta","transaction","timeout","configuration","quarkus"],"backgroundTag":"transaction-timeout-misconfigured","analyzedSha":"e1c734241f34c7919086ceb4c9262b4a58f6de44","analyzedAt":"2026-09-05T17:01:29.979Z","contentChangedAt":"2026-09-05T17:01:29.979Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}