Skip to main content
Idempotency ensures that retrying a failed request doesn’t accidentally send duplicate emails. This is critical for reliable email delivery in distributed systems.

The Problem

Network issues can cause ambiguous failures:
Client ──▶ Request ──▶ Ark ──▶ Email Accepted
       ◀── Timeout ──
Did the email send or not? Without idempotency, retrying might send the email twice.

The Solution

Include an idempotency key with your request. If you retry with the same key, Ark returns the original response instead of processing again.
from ark import Ark

client = Ark()

idempotency_key = f"order-confirmation-{order_id}"

email = client.emails.send(
    from_="[email protected]",
    to=[customer.email],
    subject=f"Order Confirmation #{order_id}",
    html=order_confirmation_html,
    idempotency_key=idempotency_key
)

How It Works

  1. First request: Ark processes the request and caches the response with the key
  2. Retry (same key): Ark returns the cached response without reprocessing
  3. Different key: Ark processes as a new request
Request 1 (key: abc123)
  └── Process ──▶ Send email ──▶ Cache response
       └── Return: { id: "msg_xyz", status: "pending" }

Request 2 (key: abc123) // Same key, retry
  └── Check cache ──▶ Found!
       └── Return: { id: "msg_xyz", status: "pending" } // Same response

Request 3 (key: def456) // Different key
  └── Check cache ──▶ Not found
       └── Process ──▶ Send email ──▶ Cache response
            └── Return: { id: "msg_abc", status: "pending" } // New email

Key Format

Use predictable, unique identifiers:
// Good: Derived from business logic
const key = `welcome-email-${userId}`;
const key = `order-${orderId}-confirmation`;
const key = `password-reset-${userId}-${timestamp}`;

// Bad: Random on each attempt (defeats the purpose)
const key = crypto.randomUUID(); // DON'T DO THIS

Key Lifetime

Idempotency keys are valid for 24 hours. After expiration:
  • The key can be reused
  • A new request with the same key will process normally

Implementation Patterns

Basic Retry Logic

The SDKs automatically retry failed requests. Combined with idempotency keys, you get safe retries:
from ark import Ark

# SDK retries automatically with exponential backoff
client = Ark(max_retries=5)

def send_transactional_email(order_id, customer_email, content):
    # Idempotency key ensures no duplicates even across retries
    return client.emails.send(
        from_="[email protected]",
        to=[customer_email],
        subject="Order Shipped",
        html=content,
        idempotency_key=f"shipment-{order_id}"
    )

# Safe to call multiple times - only sends once
email = send_transactional_email(order_id, "[email protected]", html_content)

Job Queue Integration

With job processors like Bull, Celery, or Sidekiq:
from celery import Celery
from ark import Ark

app = Celery('tasks')
client = Ark()

@app.task(bind=True, max_retries=5, default_retry_delay=60)
def send_order_confirmation(self, order_id, customer_email, order_details):
    try:
        # Idempotency key ensures exactly-once even if job retries
        return client.emails.send(
            from_="[email protected]",
            to=[customer_email],
            subject=f"Order #{order_id} Confirmed",
            html=render_order_email(order_details),
            idempotency_key=f"order-confirmation-{order_id}"
        )
    except Exception as e:
        raise self.retry(exc=e)

# Queue the job - retries are safe with idempotency
send_order_confirmation.delay("12345", "[email protected]", order_details)

Batch Operations

Use compound keys for batch items:
def send_batch_with_idempotency(emails, batch_id):
    return client.emails.send_batch(
        emails=emails,
        idempotency_key=f"batch-{batch_id}"
    )

# If retry needed, same batch_id returns original response
result = send_batch_with_idempotency(emails, "weekly-digest-2024-01-15")

Best Practices

Keys like order-123-confirmation are more meaningful than random UUIDs.
password-reset-user123-1705312800 prevents conflicts between different email types.
Order confirmations, receipts, and password resets should never be duplicated.
Save the idempotency key in your database for debugging and audit trails.

Checking Idempotency Status

The response includes idempotency information:
{
  "success": true,
  "data": {
    "id": "msg_abc123",
    "status": "pending"
  },
  "meta": {
    "requestId": "req_xyz789",
    "idempotent": true,  // true if this was a cached response
    "originalRequestId": "req_original123"  // original request that created it
  }
}

Common Pitfalls

Don’t use timestamps alone. If your clock drifts or requests are fast, you might get collisions or miss duplicates.
Don’t reuse keys for different content. The cached response is returned regardless of the request body.
// WRONG: Same key, different content
await sendEmail({ to: '[email protected]', ... }, 'key-123');
await sendEmail({ to: '[email protected]', ... }, 'key-123'); // Returns first response!

// RIGHT: Different keys for different emails
await sendEmail({ to: '[email protected]', ... }, 'welcome-user-a');
await sendEmail({ to: '[email protected]', ... }, 'welcome-user-b');

API Reference

See idempotency in action