flowable/flowable-engine · warning · ActivitiOptimisticLockingException
Could not lock process instance
Error message
Could not lock process instance
What it means
The engine uses optimistic DB locking to grant exclusive async-job acquisition to one executor node. updateProcessInstanceLockTime issues an UPDATE of the lock columns; if the update affects 0 rows, the expected process instance row was not in the state assumed (deleted, or lock version changed) and ActivitiOptimisticLockingException is thrown.
Source
Thrown at modules/flowable5-engine/src/main/java/org/activiti/engine/impl/persistence/entity/ExecutionEntityManager.java:226
getDbSqlSession().update("updateExecutionTenantIdForDeployment", params);
}
public void updateProcessInstanceLockTime(String processInstanceId) {
CommandContext commandContext = Context.getCommandContext();
Date expirationTime = commandContext.getProcessEngineConfiguration().getClock().getCurrentTime();
int lockMillis = commandContext.getProcessEngineConfiguration().getAsyncExecutorAsyncJobLockTimeInMillis();
GregorianCalendar lockCal = new GregorianCalendar();
lockCal.setTime(expirationTime);
lockCal.add(Calendar.MILLISECOND, lockMillis);
HashMap<String, Object> params = new HashMap<>();
params.put("id", processInstanceId);
params.put("lockTime", lockCal.getTime());
params.put("expirationTime", expirationTime);
int result = getDbSqlSession().update("updateProcessInstanceLockTime", params);
if (result == 0) {
throw new ActivitiOptimisticLockingException("Could not lock process instance");
}
}
public void clearProcessInstanceLockTime(String processInstanceId) {
HashMap<String, Object> params = new HashMap<>();
params.put("id", processInstanceId);
getDbSqlSession().update("clearProcessInstanceLockTime", params);
}
}
View on GitHub (pinned to d6d39ce1c6)
Solutions
- Treat this as a transient contention signal: re-fetch the job/process instance and retry the acquisition if it still exists.
- Verify the job's process instance still exists; if not, remove/expire the stale job rather than locking.
- Reduce executor contention: tune async executor lock time and pool size so fewer nodes grab the same jobs simultaneously.
- Upgrade to a Flowable version where job acquisition handles vanished instances gracefully (job is skipped instead of failing).
Defensive patterns
Strategy: retry
Validate before calling
Job job = managementService.createJobQuery().jobId(jobId).singleResult(); if (job == null || job.getProcessInstanceId() == null) return; // stale job, skip lock attempt
Try / catch
try {
acquireAndLock(instanceId);
} catch (ActivitiOptimisticLockingException e) {
// another executor won the race or instance vanished: back off and retry once
Thread.sleep(retryBackoffMs);
retryAcquisition(instanceId);
} Prevention
- Tune async executor lock time and pool size in clustered setups
- Purge stale jobs whose process instances no longer exist
- Keep engine versions consistent across all cluster nodes
When it happens
Trigger: Async executor/ job acquisition calls updateProcessInstanceLockTime for a process instance that was deleted concurrently by another transaction, or whose exclusive-lock row changed between read and update; multiple job executors racing for the same exclusive job.
Common situations: Clustered deployments with several async executor nodes contending for jobs; jobs whose process instance was deleted while still queued; stale job entries surviving instance deletion in older versions.
Related errors
- Optimistic locking exception (using global acquire lock) for
- Could not lock process instance
- exception for engine {} during async job acquisition: {}
- exception during timer job acquisition for engine {}. Except
- Optimistic locking exception (using global acquire lock) for
AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11).
Data as JSON: /api/errors/ef22b7b152423076.
Report an issue: GitHub.