Flowable optimistic locking exceptions

FlowableOptimisticLockingException appears when two transactions change the same execution or process instance at once. This page explains the three common causes, how to confirm which one you have, and the modeling and retry fixes.

On this page

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.

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

OptionWhen it fitsTrade-offRisk if misused
Exclusive async jobs (default)Parallel branches that join, or any async work on one instance that writes the same variables and execution treeWork on one instance runs one job at a time, so throughput per instance is lowerTurning exclusivity off without a join audit can reintroduce optimistic locking failures at merge points
Non-exclusive async jobsIndependent branches that write disjoint data and never converge on a shared join or variableHigher parallelism per instance, with more jobs contending for the same rowsConcurrent jobs update the same execution or variables, causing lock failures or lost updates that surface only under load
Async flag on selected activitiesMoving slow service calls or timers off the caller's transaction so a single write does not span external latencyAdds job rows and an extra commit boundary, which means more database writes per stepMarking a join or gateway as async without reviewing what writes it makes can move the collision rather than remove it
Async executor retry settingsTransient failures such as brief lock contention, where the job should be retried after a delayRetries cost database work and delay the process, and they hide the cause if nothing is monitoredAggressive retries on a true modeling conflict repeat the same failure and fill the failed-job state
Application-side serialization around API callsExternal callers that update the same instance, such as a user action and a message arriving togetherCallers wait on each other, and the serialization key must be designed carefullyA missing or wrong key leaves the race in place, and a lock held too long stalls callers
Application-side retry around API callsShort, idempotent calls that can safely be repeated after a conflict is reportedAdds latency to the caller and requires logic to detect the conflict typeRetrying 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 statusRequires design time and changes how the process is modeled and testedSkipping 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.

Talk to our team.

Tell us what you're working on, whether it's a deployment, an audit, a security test or a cyber range. You'll speak with an engineer who can help you scope it.

  • 30-minute call: free, with no obligation.
  • NDA on request: we can sign before you share details.
  • Clear next steps: a scope and plan after the call.