This Elasticsearch error occurs when you try to update or delete a document with an outdated version number. The document has been modified by another process since you last read it, causing a version mismatch. This is Elasticsearch's optimistic concurrency control preventing data loss.
The "VersionConflictEngineException" error is Elasticsearch's optimistic concurrency control mechanism in action. When you update or delete a document, Elasticsearch checks that the version number you provide matches the current version stored in the index. This error means: 1. You're trying to modify document with ID "1" (as shown by [doc][1]) 2. The document currently has version "2" in Elasticsearch 3. Your operation specified version "1" 4. Since version "2" ≠ version "1", Elasticsearch rejects the operation to prevent overwriting newer changes This typically happens when: - Multiple clients/threads are updating the same document concurrently - Your application reads a document, processes it, then tries to update it with stale version info - You're retrying a failed update operation without refreshing the document version - Another process (like Kibana, Logstash, or a different application instance) modified the document Elasticsearch uses version numbers to ensure data consistency in distributed systems. Each document update increments the version, and operations must provide the correct version to succeed.
Always retrieve the current version before attempting updates. Use the _source and _version fields together:
# Get document with its current version
curl -X GET "localhost:9200/my-index/_doc/1" -u "username:password"
# Response includes version:
{
"_index": "my-index",
"_id": "1",
"_version": 2, # <-- Current version
"_seq_no": 5,
"_primary_term": 1,
"found": true,
"_source": {
"title": "My Document",
"content": "Some content"
}
}
# Use this version in your update
curl -X POST "localhost:9200/my-index/_update/1" -u "username:password" -H 'Content-Type: application/json' -d'
{
"doc": {
"content": "Updated content"
},
"version": 2 # <-- Use the version from GET response
}
'In application code:
const { Client } = require('@elastic/elasticsearch');
const client = new Client({ node: 'http://localhost:9200' });
async function safeUpdate(documentId, updates) {
try {
// 1. Get current document with version
const getResponse = await client.get({
index: 'my-index',
id: documentId
});
const currentVersion = getResponse._version;
// 2. Update with current version
const updateResponse = await client.update({
index: 'my-index',
id: documentId,
body: {
doc: updates,
version: currentVersion
}
});
return updateResponse;
} catch (error) {
if (error.meta?.body?.error?.type === 'version_conflict_engine_exception') {
console.error('Version conflict for document', documentId);
// Handle conflict (retry, notify user, etc.)
}
throw error;
}
}For more robust concurrency control, use if_seq_no and if_primary_term instead of version numbers:
# Get document with seq_no and primary_term
curl -X GET "localhost:9200/my-index/_doc/1" -u "username:password"
# Response includes:
{
"_index": "my-index",
"_id": "1",
"_version": 2,
"_seq_no": 5, # <-- Sequence number
"_primary_term": 1, # <-- Primary term
"found": true,
"_source": { ... }
}
# Update using seq_no and primary_term
curl -X POST "localhost:9200/my-index/_update/1" -u "username:password" -H 'Content-Type: application/json' -d'
{
"doc": {
"content": "Updated content"
}
}'
-H 'If-Seq-No: 5' -H 'If-Primary-Term: 1'
# Or in the request body for update API
curl -X POST "localhost:9200/my-index/_update/1" -u "username:password" -H 'Content-Type: application/json' -d'
{
"if_seq_no": 5,
"if_primary_term": 1,
"doc": {
"content": "Updated content"
}
}
'In application code:
async function safeUpdateWithSequence(documentId, updates) {
try {
// 1. Get current document with seq_no and primary_term
const getResponse = await client.get({
index: 'my-index',
id: documentId
});
const seqNo = getResponse._seq_no;
const primaryTerm = getResponse._primary_term;
// 2. Update with sequence control
const updateResponse = await client.update({
index: 'my-index',
id: documentId,
if_seq_no: seqNo,
if_primary_term: primaryTerm,
body: {
doc: updates
}
});
return updateResponse;
} catch (error) {
if (error.meta?.body?.error?.type === 'version_conflict_engine_exception') {
console.error('Sequence conflict for document', documentId);
// The document was modified by another process
}
throw error;
}
}Sequence numbers are more reliable than simple version numbers for distributed systems.
Add intelligent retry logic to handle temporary version conflicts:
async function updateWithRetry(documentId, updates, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// Get fresh document data
const getResponse = await client.get({
index: 'my-index',
id: documentId
});
// Update with latest seq_no and primary_term
const updateResponse = await client.update({
index: 'my-index',
id: documentId,
if_seq_no: getResponse._seq_no,
if_primary_term: getResponse._primary_term,
body: {
doc: updates
},
refresh: 'wait_for' // Wait for refresh to ensure consistency
});
return updateResponse;
} catch (error) {
lastError = error;
if (error.meta?.body?.error?.type === 'version_conflict_engine_exception') {
console.log('Version conflict on attempt ' + attempt + '/' + maxRetries + ' for document ' + documentId);
if (attempt < maxRetries) {
// Exponential backoff: wait longer each retry
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
continue; // Try again
}
}
// Not a version conflict or out of retries
throw error;
}
}
throw lastError; // All retries failed
}
// For bulk operations with conflict handling
async function bulkUpdateWithConflictHandling(operations) {
const response = await client.bulk({
body: operations,
refresh: true
});
if (response.errors) {
const retryOperations = [];
response.items.forEach((item, index) => {
if (item.update?.error?.type === 'version_conflict_engine_exception') {
// Extract the failed operation for retry
const originalOp = operations[index * 2]; // bulk format: {update: {...}}, then {doc: {...}}
retryOperations.push(originalOp);
}
});
if (retryOperations.length > 0) {
console.log('Retrying ' + retryOperations.length + ' failed operations');
// Implement retry logic for failed operations
}
}
return response;
}Key retry strategies:
- Exponential backoff to avoid overwhelming the cluster
- Limit maximum retries to prevent infinite loops
- Log conflicts for monitoring and debugging
- Consider whether to merge changes or overwrite based on business logic
Elasticsearch's Update API has built-in retry functionality:
# Update with automatic retry on conflict (up to 3 times)
curl -X POST "localhost:9200/my-index/_update/1" -u "username:password" -H 'Content-Type: application/json' -d'
{
"doc": {
"content": "Updated content"
},
"retry_on_conflict": 3
}
'
# For scripted updates with retry
curl -X POST "localhost:9200/my-index/_update/1" -u "username:password" -H 'Content-Type: application/json' -d'
{
"script": {
"source": "ctx._source.views += params.increment",
"params": {
"increment": 1
}
},
"retry_on_conflict": 5,
"upsert": {
"views": 1
}
}
'In application code:
// Simple update with retry
await client.update({
index: 'my-index',
id: '1',
body: {
doc: { content: 'Updated' },
retry_on_conflict: 3
}
});
// For counter-like operations, use scripts
await client.update({
index: 'my-index',
id: '1',
body: {
script: {
source: 'ctx._source.counter += params.increment',
params: { increment: 1 }
},
retry_on_conflict: 5,
upsert: { counter: 1 }
}
});
// Bulk updates with retry
await client.bulk({
body: [
{ update: { _index: 'my-index', _id: '1', retry_on_conflict: 3 } },
{ doc: { status: 'active' } },
{ update: { _index: 'my-index', _id: '2', retry_on_conflict: 3 } },
{ doc: { status: 'inactive' } }
]
});The retry_on_conflict parameter tells Elasticsearch to automatically retry the operation if a version conflict occurs. This is useful for:
- Counter increments
- Status flag updates
- Simple field modifications where conflicts are acceptable
- High-throughput scenarios where some conflicts are expected
Note: For complex updates requiring business logic, manual retry with fresh data is better.
Architect your data to reduce version conflicts:
// 1. Use append-only patterns where possible
// Instead of updating documents, append events
const event = {
timestamp: new Date().toISOString(),
type: 'user_action',
data: { action: 'click', element: 'button' }
};
await client.index({
index: 'user-events-2024.01.01',
body: event
});
// 2. Use separate documents for frequently updated counters
await client.index({
index: 'counters',
id: 'user-1-clicks',
body: {
user_id: 'user-1',
metric: 'clicks',
count: 1,
last_updated: new Date().toISOString()
}
});
// Update counter with script (atomic, less conflict-prone)
await client.update({
index: 'counters',
id: 'user-1-clicks',
body: {
script: {
source: 'ctx._source.count += params.increment; ctx._source.last_updated = params.timestamp',
params: {
increment: 1,
timestamp: new Date().toISOString()
}
},
retry_on_conflict: 5,
upsert: {
user_id: 'user-1',
metric: 'clicks',
count: 1,
last_updated: new Date().toISOString()
}
}
});
// 3. Partition data to reduce contention
// Instead of one document for all users, use one per user
const userId = 'user-123';
await client.update({
index: 'user-profiles',
id: userId, // Each user has their own document
body: {
doc: { last_login: new Date().toISOString() },
retry_on_conflict: 3
}
});
// 4. Use external versioning for external data sources
await client.index({
index: 'external-data',
id: 'record-1',
body: record,
version: externalVersion, // From external system
version_type: 'external' // External versioning
});
// 5. Consider using Elasticsearch's built-in concurrency features
// For real-time analytics, use aggregations instead of updating counters
const searchResponse = await client.search({
index: 'user-events-*',
body: {
query: { match: { user_id: 'user-1' } },
aggs: {
total_clicks: { value_count: { field: 'event_id' } }
}
}
});Design principles to reduce conflicts:
- Append-only: Add new documents instead of updating existing ones
- Partitioning: Spread updates across many documents instead of few
- Atomic operations: Use scripts for counter-like updates
- Event sourcing: Store state changes as events, reconstruct state via queries
- Read-heavy design: Optimize for reads, minimize writes
## Advanced Version Conflict Management
### External Versioning
When syncing data from external systems, use external versioning:
# Index with external version (e.g., from database timestamp)
curl -X PUT "localhost:9200/my-index/_doc/1" -u "username:password" -H 'Content-Type: application/json' -d'
{
"title": "Document",
"updated_at": "2024-01-01T12:00:00Z"
}
' -H 'version: 1641038400000' -H 'version_type: external'
# Update only if external version is greater
curl -X PUT "localhost:9200/my-index/_doc/1" -u "username:password" -H 'Content-Type: application/json' -d'
{
"title": "Updated Document",
"updated_at": "2024-01-02T12:00:00Z"
}
' -H 'version: 1641124800000' -H 'version_type: external.gte'External versioning is useful for:
- Database synchronization
- CDC (Change Data Capture) pipelines
- Multi-system data consistency
### Conflict Resolution Strategies
1. Last Write Wins (LWW): Use timestamps to determine which update to keep
2. Merge Conflicts: Combine changes from conflicting updates
3. Business Logic Wins: Apply domain-specific rules to resolve conflicts
4. Manual Resolution: Flag conflicts for human intervention
### Monitoring and Alerting
Monitor version conflict rates:
# Check cluster stats for version conflicts
curl -X GET "localhost:9200/_cluster/stats" -u "username:password" | grep -i version
# Use Elasticsearch monitoring
# 1. Enable monitoring in kibana.yml
# 2. Check Monitoring → Elasticsearch → Advanced → Version Conflicts
# Create alert for high conflict rate
PUT _watcher/watch/version-conflict-alert
{
"trigger": { "schedule": { "interval": "5m" } },
"input": {
"search": {
"request": {
"indices": ["*"],
"body": {
"query": {
"bool": {
"must": [
{ "term": { "log.level": "error" } },
{ "wildcard": { "message": "*VersionConflictEngineException*" } }
]
}
},
"aggs": {
"conflicts_per_minute": {
"date_histogram": {
"field": "@timestamp",
"calendar_interval": "1m"
}
}
}
}
}
}
},
"condition": {
"compare": {
"ctx.payload.aggregations.conflicts_per_minute.buckets[-1].doc_count": {
"gte": 100
}
}
},
"actions": {
"send_email": {
"email": {
"to": ["[email protected]"],
"subject": "High version conflict rate detected",
"body": "Version conflicts exceeded threshold: {{ctx.payload.aggregations.conflicts_per_minute.buckets[-1].doc_count}} in last minute"
}
}
}
}### Performance Considerations
- Version tracking overhead: Each document version adds metadata overhead
- Conflict retry impact: Automatic retries increase cluster load
- Sequence number generation: High update rates can exhaust sequence number ranges
- Shard allocation: Hot documents (frequently updated) can cause shard imbalance
### Best Practices
1. Use `if_seq_no` and `if_primary_term` instead of simple version numbers
2. Set reasonable `retry_on_conflict` limits (3-5 retries typically sufficient)
3. Monitor conflict rates as a system health metric
4. Design for idempotency - operations should be safe to retry
5. Use bulk APIs with error handling for batch operations
6. Consider data modeling to reduce hot document contention
7. Implement application-level conflict resolution for complex business logic
QueryShardException: No mapping found for [field] in order to sort on
How to fix "QueryShardException: No mapping found for field in order to sort on" in Elasticsearch
IndexNotFoundException: no such index [index_name]
How to fix "IndexNotFoundException: no such index [index_name]" in Elasticsearch
DocumentMissingException: [index][type][id]: document missing
DocumentMissingException: Document missing
ParsingException: Unknown key for a START_OBJECT in [query]
How to fix "ParsingException: Unknown key for a START_OBJECT in [query]" in Elasticsearch
AggregationExecutionException: Aggregation [agg_name] does not support sampling
How to fix "AggregationExecutionException: Aggregation [agg_name] does not support sampling" in Elasticsearch