Skip to main content

Idempotency

Idempotency ensures that retrying a request doesn't create duplicate jobs. This guide explains how to use idempotency keys to build reliable integrations.

What Is Idempotency?​

An idempotent operation produces the same result whether you call it once or multiple times. Without idempotency, network issues can cause problems:

With idempotency keys, retries return the existing job instead of creating a new one:

Using Idempotency Keys​

Include the idempotency_key parameter when submitting a job:

curl -X POST https://api.cmfy.cloud/v1/jobs \
-H "Authorization: Bearer sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"prompt": {...},
"idempotency_key": "user-123-order-456-image-1"
}'

If you submit the same key again within 24 hours:

  • Job still queued/running: Returns the existing job ID
  • Job completed: Returns the completed result
  • Job failed: Returns the failure details

Generating Idempotency Keys​

Keys should be unique per logical operation. Common approaches:

UUIDs (Simplest)​

const { randomUUID } = require('crypto');

const idempotencyKey = randomUUID();
// e.g., "550e8400-e29b-41d4-a716-446655440000"

Derive keys from your application's data for automatic deduplication:

import hashlib
import json

def generate_idempotency_key(user_id, action, params):
"""Generate deterministic key from operation details."""
data = {
'user_id': user_id,
'action': action,
'params': params
}
content = json.dumps(data, sort_keys=True)
return hashlib.sha256(content.encode()).hexdigest()[:32]

# Same inputs always produce the same key
key = generate_idempotency_key(
user_id="user-123",
action="generate-avatar",
params={"style": "anime", "seed": 42}
)
# Always: "a1b2c3d4e5f6..."

Compound Keys​

Combine identifiers from your system:

function createIdempotencyKey(userId, requestId, attempt = 0) {
return `${userId}-${requestId}-${attempt}`;
}

// First attempt
const key = createIdempotencyKey("user-123", "req-456");
// "user-123-req-456-0"

// Retry after failure
const retryKey = createIdempotencyKey("user-123", "req-456", 1);
// "user-123-req-456-1" (different key, new job)

Key Requirements​

RequirementDetails
Max length255 characters
CharactersAlphanumeric, hyphens, underscores
UniquenessUnique per user within 24 hours
ExpirationKeys expire after 24 hours

Behavior by Job Status​

When you resubmit a request with the same idempotency key:

Current StatusResponseHTTP Code
queuedReturn existing job (status: queued)200
runningReturn existing job (status: running)200
completedReturn existing job (status: completed)200
failedReturn existing job (status: failed)200
cancelledReturn existing job (status: cancelled)200

The response includes an idempotent: true flag:

{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"idempotent": true,
"outputs": {
"images": ["https://cdn.cmfy.cloud/..."]
}
}

Implementation Examples​

Basic Retry Logic​

import time
import uuid

def submit_job_with_retry(workflow, max_retries=3):
"""Submit job with idempotency for safe retries."""
idempotency_key = str(uuid.uuid4())

for attempt in range(max_retries):
try:
response = requests.post(
'https://api.cmfy.cloud/v1/jobs',
headers={'Authorization': f'Bearer {API_KEY}'},
json={
'prompt': workflow,
'idempotency_key': idempotency_key
},
timeout=30
)

if response.status_code == 429:
# Rate limited - wait and retry with same key
retry_after = response.json()['error'].get('retry_after', 60)
time.sleep(retry_after)
continue

response.raise_for_status()
return response.json()

except requests.exceptions.Timeout:
# Network timeout - retry with same key (idempotent)
continue

except requests.exceptions.ConnectionError:
# Connection failed - retry with same key
time.sleep(2 ** attempt)
continue

raise Exception("Max retries exceeded")

Deterministic Job Submission​

const crypto = require('crypto');

class JobSubmitter {
constructor(apiKey) {
this.apiKey = apiKey;
}

generateKey(userId, workflow, options = {}) {
// Create deterministic key from inputs
const content = JSON.stringify({
userId,
workflow,
...options
});
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 32);
}

async submit(userId, workflow, options = {}) {
const idempotencyKey = this.generateKey(userId, workflow, options);

const response = await fetch('https://api.cmfy.cloud/v1/jobs', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
prompt: workflow,
idempotency_key: idempotencyKey,
...options
})
});

const data = await response.json();

if (data.idempotent) {
console.log(`Returned existing job: ${data.job_id}`);
} else {
console.log(`Created new job: ${data.job_id}`);
}

return data;
}
}

// Usage
const submitter = new JobSubmitter(process.env.API_KEY);

// First call creates job
await submitter.submit('user-123', workflow);

// Same call returns existing job
await submitter.submit('user-123', workflow);

Handling Different Outcomes​

def process_generation_request(user_id, prompt_config):
"""Process a generation request with proper idempotency."""

# Generate deterministic key
idempotency_key = generate_idempotency_key(
user_id=user_id,
action='generate',
params=prompt_config
)

response = requests.post(
'https://api.cmfy.cloud/v1/jobs',
headers={'Authorization': f'Bearer {API_KEY}'},
json={
'prompt': build_workflow(prompt_config),
'idempotency_key': idempotency_key,
'webhook_url': 'https://my-app.com/webhooks/cmfy'
}
)

data = response.json()

if data.get('idempotent'):
# This was a duplicate request
logger.info(f"Duplicate request, returning existing job {data['job_id']}")

if data['status'] == 'completed':
# Already done, return results immediately
return {'status': 'completed', 'outputs': data['outputs']}

elif data['status'] == 'failed':
# Previous attempt failed
# You might want to create a new job with different key
return {'status': 'failed', 'error': data['error']}

else:
# Still processing
return {'status': 'pending', 'job_id': data['job_id']}

else:
# New job created
logger.info(f"Created new job {data['job_id']}")
return {'status': 'pending', 'job_id': data['job_id']}

Use Cases​

1. Safe Retries After Timeouts​

def submit_with_safe_retry(workflow):
key = str(uuid.uuid4())

for i in range(3):
try:
return api.submit(workflow, idempotency_key=key)
except TimeoutError:
# Safe to retry - same key prevents duplicates
continue
raise Exception("Failed after 3 attempts")

2. Prevent Double-Clicks​

async function handleGenerateClick(settings) {
// Use settings hash as key - double clicks are deduplicated
const key = hashSettings(settings);

const result = await submitJob(workflow, { idempotency_key: key });

if (result.idempotent && result.status === 'completed') {
// Show existing result immediately
displayResult(result);
} else {
// Show loading state
showLoading(result.job_id);
}
}

3. Queue Processing​

def process_queue_item(item):
# Use queue item ID as idempotency key
key = f"queue-{item.id}"

result = api.submit(
item.workflow,
idempotency_key=key
)

if result.get('idempotent'):
# Already processed (maybe worker crashed and restarted)
logger.info(f"Item {item.id} already submitted as job {result['job_id']}")
else:
logger.info(f"Submitted item {item.id} as new job {result['job_id']}")

return result['job_id']

4. Batch Processing with Resume​

def process_batch(items):
"""Process batch with resume capability."""
results = {}

for item in items:
# Deterministic key allows resuming after crash
key = f"batch-{batch_id}-item-{item.id}"

result = api.submit(item.workflow, idempotency_key=key)

if result.get('idempotent'):
print(f"Item {item.id}: reusing job {result['job_id']}")
else:
print(f"Item {item.id}: created job {result['job_id']}")

results[item.id] = result['job_id']

return results

Common Mistakes​

Using Random Keys for Retries​

# Wrong: New key for each retry creates duplicate jobs
for i in range(3):
key = str(uuid.uuid4()) # ❌ Different key each time
try:
return api.submit(workflow, idempotency_key=key)
except TimeoutError:
continue

# Correct: Same key for all retries
key = str(uuid.uuid4()) # ✓ Generate once
for i in range(3):
try:
return api.submit(workflow, idempotency_key=key)
except TimeoutError:
continue

Reusing Keys for Different Operations​

# Wrong: Same key for different workflows
key = "user-123-generate"
api.submit(workflow_a, idempotency_key=key)
api.submit(workflow_b, idempotency_key=key) # ❌ Returns workflow_a's job!

# Correct: Unique key per operation
api.submit(workflow_a, idempotency_key="user-123-workflow-a")
api.submit(workflow_b, idempotency_key="user-123-workflow-b")

Ignoring the Idempotent Flag​

# Wrong: Assuming every response is a new job
result = api.submit(workflow, idempotency_key=key)
notify_user("Your job is processing!") # ❌ Might already be done!

# Correct: Check idempotent flag
result = api.submit(workflow, idempotency_key=key)
if result.get('idempotent') and result['status'] == 'completed':
notify_user("Here are your results!")
else:
notify_user("Your job is processing!")

What's Next?​

Was this page helpful?