{"record":{"id":"9bd2df4a8e7a264a","repo":"hibernate/hibernate-orm","slug":"connection-lock-timeout-does-not-accept-skip-locke-9bd2df","errorCode":null,"errorMessage":"Connection lock-timeout does not accept skip-locked","messagePattern":"Connection lock-timeout does not accept skip-locked","errorType":"exception","errorClass":"HibernateException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/PostgreSQLLockingSupport.java","lineNumber":120,"sourceCode":"\t\t\t\t\t\tcase \"h\" -> Timeout.seconds( amount * 3600 );\n\t\t\t\t\t\tcase \"d\" -> Timeout.seconds( amount * 3600 * 24 );\n\t\t\t\t\t\tdefault -> throw new IllegalArgumentException(\n\t\t\t\t\t\t\t\"Unexpected PostgreSQL lock_timeout format: \" + value );\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t\tconnection,\n\t\t\t\tfactory\n\t\t);\n\t}\n\n\t@Override\n\tpublic void setLockTimeout(Timeout timeout, Connection connection, SessionFactoryImplementor factory) {\n\t\tHelper.setLockTimeout(\n\t\t\t\ttimeout,\n\t\t\t\t(t) -> {\n\t\t\t\t\tfinal int milliseconds = timeout.milliseconds();\n\t\t\t\t\tif ( milliseconds == SKIP_LOCKED_MILLI ) {\n\t\t\t\t\t\tthrow new HibernateException( \"Connection lock-timeout does not accept skip-locked\" );\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( milliseconds == NO_WAIT_MILLI ) {\n\t\t\t\t\t\tthrow new HibernateException( \"Connection lock-timeout does not accept no-wait\" );\n\t\t\t\t\t}\n\t\t\t\t\treturn milliseconds == WAIT_FOREVER_MILLI\n\t\t\t\t\t\t\t? 0\n\t\t\t\t\t\t\t: milliseconds;\n\t\t\t\t},\n\t\t\t\t\"set local lock_timeout = %s\",\n\t\t\t\tconnection,\n\t\t\t\tfactory\n\t\t);\n\t}\n\n\tprivate static int findUnitStartIndex(String value) {\n\t\tfor ( int i = value.length() - 1; i >= 0; i-- ) {\n\t\t\tif ( Character.isDigit( value.charAt( i ) ) ) {","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/dialect/lock/internal/PostgreSQLLockingSupport.java#L102-L138","documentation":"On PostgreSQL, Hibernate applies pessimistic-lock timeouts by executing 'set local lock_timeout = N' (milliseconds) via PostgreSQLLockingSupport.setLockTimeout. In PostgreSQL lock_timeout=0 means the timeout is disabled (wait forever), so the SKIP_LOCKED magic value (-2 ms) has no connection-level representation and is rejected with this HibernateException rather than being silently converted. PostgreSQL reports Level.SUPPORTED (not EXTENDED), so skip-locked is never accepted on this path even though PG supports FOR UPDATE SKIP LOCKED as a clause.","triggerScenarios":"session.buildLockRequest(LockOptions.UPGRADE_SKIPLOCKED).lock(entity) / lockOptions.setTimeOut(-2); em.find(id, PESSIMISTIC_WRITE, hints) or query.setLockMode(PESSIMISTIC_WRITE) with 'jakarta.persistence.lock.timeout' = -2; Timeouts.SKIP_LOCKED reaching setLockTimeout when the dialect routes the timeout through the connection (LockTimeoutHandler registered).","commonSituations":"Skip-locked poller/queue patterns configured via the JPA lock.timeout hint instead of the locking clause; global 'jakarta.persistence.lock.timeout' = -2 property in persistence.xml; porting code from databases where the hint works; Hibernate 6 -> 7 migrations that now funnel lock timeouts through ConnectionLockTimeoutStrategy.","solutions":["Use a real positive timeout (e.g. 1000 ms) or WAIT_FOREVER (-1) for connection-level timeouts on PostgreSQL","Get skip-locked semantics from the locking clause: let Hibernate emit 'FOR UPDATE SKIP LOCKED' (LockMode.UPGRADE_SKIPLOCKED applied via the locking-clause strategy) or use a native query, rather than from the lock timeout","Check getConnectionLockTimeoutStrategy().getSupportedLevel() before passing magic timeout values","Remove -2 lock timeout hints from shared configuration"],"exampleFix":"// before\nMap<String, Object> hints = Map.of(\"jakarta.persistence.lock.timeout\", -2);\nList<Task> tasks = em.createQuery(select t from Task t ..., Task.class)\n        .setLockMode(LockModeType.PESSIMISTIC_WRITE).setHints(hints)\n        .setMaxResults(10).getResultList();\n\n// after: rely on skip-locked via follow-on locking without the connection-timeout hint,\n// or use a short real wait\nMap<String, Object> hints = Map.of(\"jakarta.persistence.lock.timeout\", 1000);","handlingStrategy":"validation","validationCode":"int millis = lockOptions.getTimeOut();\nif (millis == Timeouts.SKIP_LOCKED_MILLI) {\n    lockOptions.setTimeOut(1000); // connection lock_timeout cannot express skip-locked on PG\n}","typeGuard":"static boolean acceptsConnectionTimeout(ConnectionLockTimeoutStrategy s, int millis) {\n    if (s.getSupportedLevel() == ConnectionLockTimeoutStrategy.Level.NONE) return false;\n    if (millis == Timeouts.SKIP_LOCKED_MILLI) return false;\n    return millis != Timeouts.NO_WAIT_MILLI\n            || s.getSupportedLevel() == ConnectionLockTimeoutStrategy.Level.EXTENDED;\n}","tryCatchPattern":"try {\n    session.buildLockRequest(lockOptions).lock(entity);\n} catch (HibernateException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"lock-timeout does not accept\")) {\n        lockOptions.setTimeOut(1000);\n        session.buildLockRequest(lockOptions).lock(entity);\n    } else { throw e; }\n}","preventionTips":["On PostgreSQL, obtain skip-locked via the FOR UPDATE SKIP LOCKED clause, not the lock timeout value","Do not set jakarta.persistence.lock.timeout to -2 in shared config","Verify dialect support level before using magic timeout values","Cover locking flows with tests running on the production database"],"tags":["postgresql","pessimistic-locking","lock-timeout","skip-locked","hibernate"],"backgroundTag":"pessimistic-lock-timeout-unsupported","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}