Why Flowable optimistic locking exceptions appear under concurrent load
A FlowableOptimisticLockingException means the engine tried to update a runtime row and found that another transaction had already changed it, so the version check it performs no longer matched. In practice, two threads or nodes wrote to the same process instance, execution or task at nearly the same moment, and the second writer lost. The exception is rarely random. It appears when volume rises because that is when two writers are more likely to reach the same row before either commits. Flowable stores a version number on its runtime rows and includes it in each update, so a stale write is rejected rather than silently overwriting state. Treat the exception as a symptom of contention on one instance, not automatically as a defect in the engine.
You need this page when the exception shows up in logs or failed jobs and you cannot yet say which writers are colliding. The usual sources are parallel branches that converge at a join, asynchronous continuations or timers that fire while another job is still working on the same instance, and API callers that complete tasks, set variables or signal events on an instance that a job is also modifying. Each source has a different fix, so the first task is to identify which path produced the conflicting writes. Confirm your Flowable version and database type against the Flowable configuration docs, then check whether the failing jobs share a process instance ID and an activity. That pattern tells you whether the problem sits in the model, the job configuration or the calling application.
Three causes that trigger the exception most often
Each cause makes two transactions write to the same process instance or execution tree at nearly the same time, and the second writer loses. The cards below cover the join, async continuations and API callers, plus the retry behavior that can repeat a conflict. Start with the activity named in the exception, then check which writers touched that instance in the same window.
Parallel branches meeting at a join
A parallel gateway waits until every incoming branch arrives, then continues the process. When two branches reach the join at nearly the same moment, both transactions try to update the same parent execution, and one fails with the exception. The failure names the gateway activity in the stack trace and tends to appear only under load, because a single test run keeps branches apart. First check: confirm which activity the exception names, then look at whether the branches contain service tasks or async steps that finish close together.
Async continuations in separate transactions
An activity marked async, or a job the engine creates after an async step, runs in its own transaction and reloads the execution before it writes. If another job or API call changes the same execution tree in between, the job's write fails. The symptom is an exception in the job executor log followed by a retry, often on a job that succeeds on the second attempt. First check: list the async jobs for the affected instance, note which activity each one continues, and compare their creation times with the failing timestamps.
API callers writing to one instance
Applications that complete tasks, set variables or send signals through the REST or Java API can target the same process instance from several threads or services. Two calls that each read the instance state and write it back will collide, and the loser receives the exception. Retrying clients, duplicate webhook deliveries and front ends that submit twice make this more likely. First check: group your API logs by process instance ID, look for two or more writes inside a short window, and confirm whether the client retries on error.
Retries that repeat the conflict
A failed job returns to the queue and the executor tries it again after the retry interval. If the conflicting transaction has not finished, the retry collides again. Once the retries run out, the job stays in a failed state for an operator to handle, so one early conflict can look like a pattern of failures. First check: count failed jobs for the same instance and activity, and find the earliest failure in the sequence, since that is the conflict to investigate.
How to confirm which cause you have in your logs and data
Join failures usually show up when two or more parallel branches of one process instance reach the same parallel gateway or join point and both try to update the execution tree and the instance's variables at nearly the same moment. The exception stack trace usually points into engine code that updates the execution, so the origin you need is the process definition and the activity ID where the join sits, not the engine frame itself. Start there and work outward.
- Stack trace origin: Record the top application frames, the activity ID named in the exception or log line, and whether the failing thread ran a synchronous request or an async job.
- Process definition: Capture the definition key and version so you can open the exact model that produced the join.
- Activity ID: Note the parallel gateway or inclusive join where branches meet, and whether more than one branch writes the same process variable.
- Timing: Correlate the exception timestamps with the branch completion times in history data to see whether branches finished within the same moment.
- Concurrent requests: Check whether a user action or API call touched the same instance during that window.
If the failing activity is a join and branches finish close together, the cause is most likely here.
Async job failures come from the engine's own work: asynchronous continuations, timers and retried jobs that the async executor runs from its thread pool. Two jobs for the same process instance can run at once if they are not exclusive, and that is the case where one job's commit overtakes another's. Your job is to identify the job type, the lock state and whether a retry was involved.
- Job type: Distinguish async continuations, timers and retries, since each enters the executor differently.
- Job tables: Query the runtime job table for jobs with retries remaining or a recent lock owner, and the dead-letter table for jobs that exhausted retries. Confirm table and column names against your Flowable version.
- Lock fields: Compare lock owner and lock expiration values with the exception time to see whether a lock expired while another node was working.
- Node identity: In a cluster, record which node acquired each job so you can see two nodes touching one instance.
- Monitoring data: Use the
/actuator/flowableendpoint for running process counts and related signals, and treat it as one source among the logs and tables.
If the conflicting jobs share an instance and a lock expired or a retry ran during the window, look at exclusivity and executor settings next.
API-driven failures happen when application code or external callers update the same process instance, task or variables through the Java or REST API while the engine is also advancing that instance. The exception appears on the caller's request, so the request logs are the primary evidence. The question is which two writes overlapped and what each caller was doing.
- Caller pattern: Capture the endpoint, HTTP method, user or service identity and request ID for each failing call.
- Target object: Record the process instance ID, task ID and any variable names the request modified.
- Concurrent requests: Find other requests for the same instance within the failing request's start and end times, including retries from clients or gateways.
- Stack trace origin: Note whether the exception was raised inside your delegate code, a service task or the API layer, since that narrows the write path.
- Process definition and activity: Capture the current activity when the call failed to see whether the instance was mid-transition.
If two callers or a caller and a job wrote to the same instance in one window, the fix is usually in the caller pattern or the model rather than the engine settings.
A single exception rarely identifies its cause alone. Correlation means putting the engine log, the database tables, your application request logs and any gateway or proxy logs on one timeline, keyed by process instance ID and time. Clock drift between nodes can make a sequence look reversed, so confirm time synchronization before reading the order of events.
- Common key: Use the process instance ID as the join key across logs, and add the job ID and request ID where available.
- Time window: Build a window of several seconds around each exception and list every write to the instance inside it.
- Clock sync: Confirm the time source on each node and gateway so timestamps line up.
- Repeat pattern: Count exceptions by definition, activity and caller over a longer period to see whether one source dominates.
- Reproduction: If the pattern is clear, reproduce it in a test environment with the same concurrency before changing production settings.
Ask your team to confirm log format, retention and table layouts for your Flowable version, since these differ between releases and deployment setups.
How to fix the exception without hiding data loss
This sequence moves from reproducing the conflict in a test environment to a model or API fix, so the exception is removed by design rather than suppressed by retries. Each step ends with a check that confirms the fix holds under load.
Reproduce the conflict under load
Rebuild the failing path in a test environment with the same process definition version, database type and async executor settings as production. Drive concurrent requests and completing jobs against the same process instance, using load that matches the production pattern (for example, parallel branches finishing together or an API caller sending repeated updates). Capture the stack trace, the process instance ID and the job or task IDs involved. The step is done when the exception reproduces on demand and the log shows which engine operation lost the version check.
Classify the cause from the stack trace
Sort the failure into one of the known patterns: parallel branches arriving at a join at the same time, asynchronous continuations that run against the same execution, or external API callers writing to the same instance. Match the operation named in the trace and the sequence of job or API calls to one pattern. Record the classification with the evidence that supports it. The step is done when each reproduced failure has one documented cause, and any failure that fits no pattern is flagged for deeper engine log review before changes are made.
Review join and async modeling
Work with the process modelers to examine the BPMN where the failing operation occurs. Check whether the join is reached by jobs that can run in parallel, and whether async flags on the join, the gateway or the tasks around it are needed. Where parallel branches converge, consider making the related jobs exclusive so the engine runs them sequentially for that process instance, and remove async flags that do not protect a real transaction boundary. Document each change and its reason in the model. The step is done when the revised model passes a rerun of the reproduction case without the conflict.
Serialize or re-read in the API layer
Where the conflict comes from an application or integration calling the engine for the same process instance, decide how callers coordinate. Options include queuing updates per instance so only one writer is active, or reading the instance again before each update so the caller works from current state. Implement the chosen approach in the calling code and avoid changing engine behavior to accommodate it. The step is done when a concurrent test sending overlapping updates for one instance completes with each update applied once and no lost changes.
Bound retries and make them idempotent
If a retry is still needed for a transient conflict, limit the number of attempts, back off between them, and log each attempt with the instance and job identifiers. Confirm that the retried operation can run twice without duplicating side effects, such as payments, notifications or external records, by using idempotency keys or checks on existing results. Retries alone can mask a design flaw, so a rising retry count in monitoring should trigger a return to the modeling review. The step is done when the retry limit, backoff and idempotency behavior are written down and tested by forcing a conflict.
Verify data integrity and load behavior
Compare process variables, history records and downstream effects between a clean run and a load run that contains conflicts. Confirm that no variable was overwritten silently, no task or branch was skipped, and no side effect occurred twice. Repeat the load test at the expected production concurrency and watch the exception count, job failures and database lock waits. The step is done when the outcome matches the clean run on the integrity checks and the exception no longer appears at the target load, with results recorded for review.
Confirm the fix in production monitoring
Deploy the revised model and application code through the normal release process, then watch the exception logs, failed job counts and retry metrics after release. Ask the engine and application owners to confirm that alerts exist for a rising optimistic locking rate and that someone is assigned to respond. Keep the load test and reproduction case in the repository so future model changes can be checked against them. The step is done when production shows no recurring conflicts for the affected flows and the monitoring and test assets are owned by named people.
Configuration and modeling options and when each fits
This table compares the levers that change how often parallel work collides on the same process instance and how the engine responds when it does. Read each row as a choice between throughput and consistency, and check the risk column before changing a setting in production. Exclusive jobs are the default protection, so most teams should change them only after they can name the collision they are trying to remove.
| Option | When it fits | Trade-off | Risk if misused |
|---|---|---|---|
| Exclusive async jobs (default) | Parallel branches that join, or any async work on one instance that writes the same variables and execution tree | Work on one instance runs one job at a time, so throughput per instance is lower | Turning exclusivity off without a join audit can reintroduce optimistic locking failures at merge points |
| Non-exclusive async jobs | Independent branches that write disjoint data and never converge on a shared join or variable | Higher parallelism per instance, with more jobs contending for the same rows | Concurrent jobs update the same execution or variables, causing lock failures or lost updates that surface only under load |
| Async flag on selected activities | Moving slow service calls or timers off the caller's transaction so a single write does not span external latency | Adds job rows and an extra commit boundary, which means more database writes per step | Marking a join or gateway as async without reviewing what writes it makes can move the collision rather than remove it |
| Async executor retry settings | Transient failures such as brief lock contention, where the job should be retried after a delay | Retries cost database work and delay the process, and they hide the cause if nothing is monitored | Aggressive retries on a true modeling conflict repeat the same failure and fill the failed-job state |
| Application-side serialization around API calls | External callers that update the same instance, such as a user action and a message arriving together | Callers wait on each other, and the serialization key must be designed carefully | A missing or wrong key leaves the race in place, and a lock held too long stalls callers |
| Application-side retry around API calls | Short, idempotent calls that can safely be repeated after a conflict is reported | Adds latency to the caller and requires logic to detect the conflict type | Retrying non-idempotent commands can complete a step twice or act on stale variables |
| Model redesign (separate instances or messages) | Work that needs to update shared state from many sources, such as a case or aggregate status | Requires design time and changes how the process is modeled and tested | Skipping redesign and stacking retries leaves the collision in the model where it will return |
How to test the fix before production
Test the fix with a load test that reproduces the two conditions that cause the exception: parallel branches arriving at the same join, and API callers changing the same process instance at the same moment. Run it on a staging copy of production-sized data, because the number of rows in the execution, job and variable tables changes how long each transaction holds its locks. Build the scenario from real production patterns: how many instances are active, how many branches each model fans out into, and which endpoints external systems call against a single instance. Confirm with your team which load tool you will use and whether it can drive both engine-internal jobs and REST calls in the same run.
Measure before and after on identical data and load. Record the rate of FlowableOptimisticLockingException in the logs, the number of failed or retried jobs, the average and tail latency of the affected transactions, and the throughput of completed instances. Run the baseline against the unchanged model and code first, then the candidate change, and keep the data set and seed identical so the comparison holds. A lower exception count is only a win if completion throughput and latency stay within the limits your operations team accepts.
Watch database lock waits during both runs. Lock behavior depends on the database you run, including the isolation level, how row locks escalate, and how the engine's update statements are written, so confirm the specifics for your database version with its vendor documentation and your DBA before you read the numbers. Capture lock wait and deadlock metrics alongside application logs, and check whether a fix moves contention from the engine's version column to a few hot rows in another table. Treat any new deadlock pattern as a failed test, even if the exception rate dropped.
Operational and security considerations while fixing concurrency
Changing joins, async boundaries and retry behavior alters how the engine and your integrations behave under load. The cards below cover the side effects that follow a fix, from database pressure and duplicate actions to audit integrity, API access and the monitoring that shows whether the problem is gone.
Retry storms and database load
Retrying a failed transaction immediately, across many threads, can turn one conflict into a burst of repeated writes against the same rows. Each retry re-reads the execution and history tables and competes for the same locks. Set a bounded number of attempts with a randomized backoff in your own retry wrapper, and check database lock wait and deadlock metrics during a load test before and after the change. Confirm which retry behavior your Flowable version applies by default.
Duplicate side effects from delegates
A transaction rollback undoes database writes but not an email, payment call, ledger posting or message sent to an external system. When a conflict forces a retry, a non-idempotent service task or delegate can run its side effect twice. Give each outbound call a stable business key, such as the process instance ID combined with the activity ID, and have the receiving system reject repeats. Review every delegate that touches a system outside the engine database.
Audit trail integrity on retries
Engine history reflects committed transactions, so a rolled-back attempt should leave no history rows, while any external audit log written by your code may keep entries from attempts that failed. Compare the engine history for a sample instance with your application audit log after a retry. If they disagree, move audit writes into the same transaction or mark each entry with the attempt number and outcome so reviewers can reconcile them.
API callers changing the same instance
When several services or users call the REST or Java APIs to complete tasks, set variables or signal the same instance, they create the conflicts that the engine reports. Each caller needs an authorization check that confirms it may act on that instance and task, and the API should return a clear conflict response that clients can retry safely. Confirm how your deployment exposes task and instance operations and which checks your application adds, since the engine leaves authorization to your code.
Control of retry and job configuration
Retry counts for async jobs and the settings that govern exclusive execution are configuration, so changes to them need the same review as code. Record who changed the job retry settings, in which environment and why. Keep the values in version-controlled configuration rather than edited directly in a running environment, and confirm that the environment separation required by your change process covers them.
Monitoring for recurring failures
A fix that removes most conflicts can still leave a steady trickle that hides in logs. Count optimistic locking exceptions by process definition, activity and job type, and alert when a definition's rate rises after a release. Use the Flowable monitoring endpoint for running process state and review failed jobs regularly, because a job that keeps failing after retries signals a modeling problem rather than a transient one.
Questions teams ask before fixing optimistic locking
How much does it cost to fix optimistic locking exceptions in Flowable?
No price is published here, and the cost comes from the written scope. The main cost is the time spent reading process models, tracing every API path that touches a running instance, and changing the models and retry logic. Cost rises with the number of process models that contain parallel joins or async steps, and with the number of API paths that update the same instances. Ask any partner for a quote that separates the model review, the code changes, the load test and the knowledge transfer, and state which of these are one-time and which recur, such as a retainer for later tuning.
How long does a fix for Flowable optimistic locking usually take?
The duration depends on how many models and API paths are involved, not on the exception itself. A small application with one join and one API caller is a different project from a platform with dozens of definitions updated by several services. Availability of a load test environment also changes the schedule, because the fix is only confirmed when the failing concurrency pattern is reproduced. Ask for a timeline tied to a written scope, with milestones for the model review, the changes, the load test and the release.
What do we need to provide before someone can start the work?
The work needs access to the deployed process definitions, the application code that calls the Flowable APIs, and the database and job tables where the exceptions appear. It also needs logs that show when the exceptions occur, the job configuration, and a named owner who can confirm business rules for joins and retries. A staging environment that resembles production and a way to generate concurrent load are important. Confirm each item with your own team before scoping, since access rules differ by organization.
Can our internal team fix optimistic locking without a partner?
Often yes, if the team already knows the Flowable engine well and can reproduce the failure. The fix is usually a combination of exclusive job settings, model changes at joins, and retry logic in the callers, which are standard engineering tasks. Internal teams often struggle with proving the fix under load and with finding every API path that touches an instance. Check whether you have a repeatable load test, someone who has operated the async executor, and time to review each process model.
What drives the effort up or down in this kind of fix?
Effort rises with the number of process models that have parallel branches meeting at joins or async continuations, because each one needs review. It also rises with the number of API paths that update the same process instance from different services or user actions. Load test availability is a major factor, since without one the fix cannot be verified and the work moves into production monitoring. Effort falls when the models are simple, the API callers are few and well documented, and a test environment already exists.
Does the fix change how our process models behave in production?
It can. Marking jobs as exclusive or changing how a join is modeled alters the order in which work runs, which can change timing and throughput. Retry logic in API callers changes how often a request is repeated and whether duplicate actions are possible, so business owners must approve the behavior. Ask the team to document each changed model, the expected runtime effect, and the rollback step before any release.
Does DeployOpen guarantee that optimistic locking exceptions will stop?
No engagement can promise a specific result, because the outcome depends on the models, the code and the load your system carries. DeployOpen plans and implements the fix within a written scope, tests it under the concurrency pattern agreed with you, and documents what changed. Ask for the test scenarios and acceptance criteria in the proposal so you can judge whether the fix meets your needs before work starts.
Will the fix require ongoing support after the initial release?
Some teams need follow-up after release, especially when new process models or API paths are added later. Support can be handled by your team with a runbook, or through a retainer for monitoring, tuning and upgrades. Ask any partner to state which parts of the work are one-time and which recur, and what the handover includes.
How DeployOpen scopes Flowable concurrency fixes
Diagnosis and fixes for Flowable optimistic locking exceptions are scoped in writing before any work starts. The scope names the engine version and deployment topology in scope, the process and case definitions that show the failure, the services and front ends that call the engine, and the environments where changes will be verified. DeployOpen's analysis covers three sources of evidence: application and engine logs that show which job or API call lost the race, the BPMN and CMMN models where parallel branches meet or where asynchronous continuations are placed, and the API usage patterns of callers that update the same process instance. Scope and timelines are agreed in writing, so you know what is covered, what is excluded and what you will receive before the engagement begins.
Deliverables are changes with verification notes. Depending on what the analysis finds, these can include model changes (such as exclusive job settings or join redesign), retry logic in calling code, changes to how clients sequence updates to one instance, and test cases that reproduce the conflict under concurrent load. Each change is accompanied by notes that record the reproduction steps, the before and after results, and the open risks you should accept or retest. Confirm during scoping how load tests will be run, which environment will be used, and who on your side approves changes to production models. Ongoing support is available through retainers if you want the same team to watch for regressions after upgrades or new process releases.

