HTTP

409 - Conflict

Hitting a 409 Conflict means your request collided with the server's current state—another process modified the resource, you tried to create a duplicate, or the operation conflicts with an in-progress transaction. This client-side error (4xx) happens when optimistic concurrency control detects state changes or business rules prevent the operation. Most common during concurrent edits where multiple users update the same resource, but also appears when creating resources with unique constraints, attempting state transitions that aren't allowed, or database transaction conflicts.

#Common Causes

  • Frontend: Multiple users editing same resource simultaneously—last write wins scenarios. Client tries to create resource that already exists (duplicate email, username). Stale ETag used for conditional update after resource changed. Concurrent API calls from same client create race conditions.
  • Backend: Optimistic locking detects ETag/version mismatch during update. Unique constraint violation (database throws duplicate key error). State machine validation fails (trying to transition from invalid state). Transaction isolation level detects concurrent modification. Business logic prevents operation (e.g., can't delete active subscription).
  • Infrastructure: Load balancer routes concurrent requests to different backend instances without shared state. Database replication lag causes temporary inconsistencies. Cache invalidation timing creates stale reads. Distributed lock service fails to acquire lock.

Solutions

  1. 1Step 1: Diagnose - Check DevTools Network tab—look for 409 response body with conflict details. Verify if multiple requests were sent simultaneously. Check ETag values in request/response headers for version mismatches.
  2. 2Step 2: Diagnose - Server logs show which constraint or state check failed. Review database unique constraint violations. Check transaction logs for concurrent modification conflicts. Look for optimistic locking rejection messages.
  3. 3Step 3: Fix - Client-side: Implement conflict resolution UI—show both versions, let user merge changes. Refresh resource and get new ETag before retry. Check for existing resources before creation (GET before POST). Use exponential backoff for retries.
  4. 4Step 4: Fix - Server-side: Return detailed conflict information in 409 response (what changed, current state). Implement ETag-based optimistic locking. Use database transactions with proper isolation levels. Add unique constraint checks before insert.
  5. 5Step 5: Fix - Infrastructure: Use distributed locks (Redis, etcd) for critical sections. Ensure database replication lag is acceptable. Implement cache invalidation strategies. Consider eventual consistency for non-critical operations.

</>Code Examples

Fetch API: Conflict Resolution with ETags
1// Client-side: Handle 409 conflicts with automatic retry
2async function updateResource(id, data, currentEtag) {
3  const response = await fetch(`/api/resources/${id}`, {
4    method: 'PUT',
5    headers: {
6      'Content-Type': 'application/json',
7      'If-Match': currentEtag, // Conditional update
8    },
9    body: JSON.stringify(data),
10  });
11  
12  if (response.status === 409) {
13    // Conflict detected - get latest version
14    const latestResponse = await fetch(`/api/resources/${id}`);
15    const latestData = await latestResponse.json();
16    const newEtag = latestResponse.headers.get('ETag');
17    
18    // Merge changes (simple merge, or show conflict UI)
19    const mergedData = mergeChanges(data, latestData);
20    
21    // Retry with merged data and new ETag
22    return updateResource(id, mergedData, newEtag);
23  }
24  
25  if (!response.ok) {
26    throw new Error(`Update failed: ${response.status}`);
27  }
28  
29  return response.json();
30}
31
32// Check for duplicates before creation
33async function createResource(data) {
34  // Check if resource already exists
35  const checkResponse = await fetch(`/api/resources?email=${data.email}`);
36  if (checkResponse.ok) {
37    const existing = await checkResponse.json();
38    if (existing.length > 0) {
39      throw new Error('Resource with this email already exists');
40    }
41  }
42  
43  // Create resource
44  const response = await fetch('/api/resources', {
45    method: 'POST',
46    headers: { 'Content-Type': 'application/json' },
47    body: JSON.stringify(data),
48  });
49  
50  if (response.status === 409) {
51    const error = await response.json();
52    throw new Error(`Conflict: ${error.message}`);
53  }
54  
55  return response.json();
56}
Express.js: Optimistic Locking and Conflict Handling
1// Server-side: Implement optimistic locking with ETags
2const express = require('express');
3const crypto = require('crypto');
4const app = express();
5
6// Generate ETag from resource
7function generateETag(resource) {
8  const hash = crypto.createHash('md5').update(JSON.stringify(resource)).digest('hex');
9  return `"${hash}"`;
10}
11
12// Update with optimistic locking
13app.put('/api/resources/:id', async (req, res) => {
14  const resource = await db.resources.findById(req.params.id);
15  
16  if (!resource) {
17    return res.status(404).json({ error: 'Resource not found' });
18  }
19  
20  // Check If-Match header for optimistic locking
21  const ifMatch = req.headers['if-match'];
22  const currentETag = generateETag(resource);
23  
24  if (ifMatch && ifMatch !== currentETag) {
25    // Conflict - resource was modified
26    return res.status(409).json({
27      error: 'Conflict',
28      message: 'Resource was modified by another request',
29      currentETag: currentETag,
30    });
31  }
32  
33  // Update resource
34  const updated = await db.resources.update(req.params.id, req.body);
35  const newETag = generateETag(updated);
36  
37  res.set('ETag', newETag);
38  res.json(updated);
39});
40
41// Create with duplicate check
42app.post('/api/resources', async (req, res) => {
43  // Check for duplicate
44  const existing = await db.resources.findByEmail(req.body.email);
45  if (existing) {
46    return res.status(409).json({
47      error: 'Conflict',
48      message: 'Resource with this email already exists',
49      existingId: existing.id,
50    });
51  }
52  
53  const resource = await db.resources.create(req.body);
54  res.status(201).json(resource);
55});
Nginx: Pass ETag Headers for Conflict Detection
1# Nginx: Ensure ETag and conditional headers reach backend
2server {
3    listen 80;
4    server_name api.example.com;
5    
6    location /api/ {
7        proxy_pass http://backend;
8        
9        # Pass conditional headers for optimistic locking
10        proxy_set_header If-Match $http_if_match;
11        proxy_set_header If-None-Match $http_if_none_match;
12        proxy_set_header If-Modified-Since $http_if_modified_since;
13        proxy_set_header If-Unmodified-Since $http_if_unmodified_since;
14        
15        # Pass ETag from backend to client
16        proxy_pass_header ETag;
17        
18        # Don't cache resources that use ETags (dynamic content)
19        proxy_cache_bypass $http_if_match $http_if_none_match;
20        add_header Cache-Control "no-store, must-revalidate";
21        
22        proxy_set_header Host $host;
23        proxy_set_header X-Real-IP $remote_addr;
24    }
25}

Related Errors

Provider Information

This error code is specific to HTTP services. For more information, refer to the official HTTP documentation.

409 - Conflict | HTTP Error Reference | Error Code Reference