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"
Deterministic Keys (Recommended)
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
| Requirement | Details |
|---|---|
| Max length | 255 characters |
| Characters | Alphanumeric, hyphens, underscores |
| Uniqueness | Unique per user within 24 hours |
| Expiration | Keys expire after 24 hours |
Behavior by Job Status
When you resubmit a request with the same idempotency key:
| Current Status | Response | HTTP Code |
|---|---|---|
| queued | Return existing job (status: queued) | 200 |
| running | Return existing job (status: running) | 200 |
| completed | Return existing job (status: completed) | 200 |
| failed | Return existing job (status: failed) | 200 |
| cancelled | Return 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?
- Error Handling - Handle errors with retries
- Webhooks - Receive results asynchronously
- Performance Best Practices - Optimize your integration