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?