← Engineering journal

Idempotency & reliable operations9 min read

Idempotency. Do it twice. Make it happen once.

Double-clicks, retries and redeliveries are normal. Duplicate bookings, payments and side effects should not be. Design WordPress operations that can safely be attempted again.

Field notes / WordPress engineering

The basic principle

Sometimes a website completes an action but the answer never reaches the person who requested it. They try again, and the system must decide whether this is a new action or another attempt at the same one. Idempotency means that repeating the same intended operation has the same effect as carrying it out once. It lets us build retries that recover from uncertainty without creating another booking, another payment or another set of changes. The difficult part is remembering the outcome reliably, even when requests overlap or a server fails halfway through.

The server completed the booking. The connection disappeared before the browser received the answer. Should the browser try again?

If another request creates another booking, retrying is dangerous. If the application recognises the same intended operation and returns the existing result, retrying becomes a recovery mechanism. That property is idempotency: repeating an operation has the same intended effect as performing it once.

It is one of the most useful ideas in application engineering because uncertainty is ordinary. People double-click, mobile connections drop, payment providers redeliver events and background workers restart. A dependable WordPress application needs a better response than hoping every request arrives exactly once.

Start with the effect, not the response

Setting a profile’s visibility to “private” is naturally idempotent if repeating the command leaves it private. Toggling visibility is not: performing it twice returns to the original state. Likewise, “set the expiry date to this value” and “extend the expiry by thirty days” have different retry behaviour.

The effect and the HTTP response need not be identical. Deleting a resource can succeed on the first attempt and report that it is absent on the next, while still satisfying the intended effect. The HTTP specification defines idempotency in terms of intended server effect; it does not prohibit separate logs for repeated requests.

HTTP method semantics are a contract to implement. Calling a route with PUT does not repair a handler that sends a new invoice every time it runs. Conversely, an application can make a POST operation retry-safe through an explicit idempotency contract.

Repeated attempts resolve to one recorded business operationFirst attemptKEY + FINGERPRINTSame intentSAME KEY / RETRYDurable receiptUNIQUE OPERATIONNew claimCOMMIT + RECORDKnown resultREPLAY OUTCOME
A retry carries the same operation key and payload. A durable receipt links it to the existing outcome instead of creating a second business operation.

A disabled submit button is helpful, but insufficient

Disabling a button while a request is pending improves the interface. It cannot prevent a retry after a timeout, a second browser tab, an automated client or a repeated delivery from another system.

For a booking form, create one operation key for the user’s current booking intent. Keep it through retries of that same intent. If the visitor deliberately starts a different booking, use a new key. Generating a fresh key on every network attempt defeats deduplication; reusing one permanent key for every booking blocks legitimate future work.

A key is not proof of identity or permission. Authenticate the caller and authorise access before resolving the operation or returning its result. Scope the key to the account and operation so one customer’s key cannot retrieve another customer’s booking.

Bind the key to what was requested

The same key with different parameters should not silently return an unrelated earlier result. Associate it with a fingerprint of the validated business input. Hash a canonical representation of the values that matter, not the raw JSON bytes whose property order may vary.

This small PHP example represents a booking request for a particular slot and quantity. It uses an explicit field order and a contract version. Authentication and the operation key belong to the surrounding request boundary, not to this pure fingerprint function.

function enjin_booking_fingerprint(int $slot_id, int $quantity): string {
    if ($slot_id < 1 || $quantity < 1 || $quantity > 10) {
        throw new InvalidArgumentException('Invalid booking input.');
    }

    $canonical = json_encode([
        'contract' => 'booking.create.v1',
        'slot_id'  => $slot_id,
        'quantity' => $quantity,
    ], JSON_THROW_ON_ERROR);

    return hash('sha256', $canonical);
}

The quantity limit is an illustrative business rule. Validate external types before calling the function; PHP’s type declarations alone are not a complete HTTP input validator. If pricing, location or a quote version defines the intent, include the relevant immutable reference. Do not accept a client-supplied total as payment authority.

A matching fingerprint identifies equivalent validated input under that contract. It does not establish that the caller is authorised or that the operation should still be performed. Recheck the current policy where the business transition requires it.

Let the database arbitrate competing first attempts

A check for an existing key followed by an unprotected insert is a race. Two workers can both observe that no receipt exists. Give the receipt a database-enforced unique identity so only one can establish ownership of that operation.

This illustrative table records a scoped key, request fingerprint and result reference. Replace the prefix and choose retention and migrations for the actual application. The binary collation makes operation keys case-sensitive; equivalent-looking keys must not be merged accidentally by a case-insensitive default.

CREATE TABLE wp_operation_receipts (
    account_id BIGINT UNSIGNED NOT NULL,
    operation VARCHAR(40) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
    idempotency_key VARCHAR(128) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
    request_hash CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
    status VARCHAR(16) NOT NULL,
    result_id BIGINT UNSIGNED NULL,
    created_at DATETIME NOT NULL,
    completed_at DATETIME NULL,
    PRIMARY KEY (account_id, operation, idempotency_key)
) ENGINE=InnoDB;

For a wholly local transactional operation, insert the receipt, reserve capacity, create the booking and mark the receipt complete within one transaction on the same connection. Roll back if any step fails. A competing insertion of the same key may wait for that transaction and then encounter the unique constraint.

Handle the specific duplicate-key outcome through a controlled read of the existing receipt. Do not classify every database exception as a duplicate, and do not assume a stale snapshot inside an existing transaction will reveal the newly committed row. Use a transaction and locking-read strategy appropriate to the engine and isolation level.

Define a response for every receipt state

A completed receipt with a matching fingerprint can return the original result reference after the caller’s access is checked. A mismatched fingerprint should produce an explicit conflict. A genuinely pending asynchronous operation can return an operation identifier and a documented way to check its status.

A pending state must have an owner and recovery policy. If work continues outside the initial transaction, “pending” does not prove that a worker is still alive. Use durable work records, bounded leases where appropriate and reconciliation that can establish whether an effect already happened before attempting it again.

If commit succeeds but the response is lost, the retry can find the completed receipt. If the process dies before commit, the transactional changes roll back and another attempt can establish the operation. A connection loss during commit creates an unknown outcome; resolve it by the operation identity rather than assuming either success or failure.

Payments cross a boundary your transaction cannot cover

A local SQL rollback cannot undo a remote charge. If the payment provider supports idempotency, persist a stable provider key for that payment intent and reuse it on uncertain retries. Stripe documents its idempotent-request contract, including parameter comparison and result replay. Read the provider’s exact retention and failure behaviour instead of assuming every API follows the same rules.

Do not hold a database transaction open while waiting for a remote payment call. Model the payment as a stateful workflow with a durable record of the intended action, the provider reference and the reconciliation status. A timeout can mean “the answer is unknown”, not “the charge failed”.

Generating a new provider key after every timeout can create the duplicate charge the key was meant to prevent. Equally, reusing the same key for a genuinely new payment can return the old result. Keys describe business intent, not merely a transport connection.

Background jobs need the same discipline

A job that sets a record to an authoritative source version can often be replayed safely. A job that increments a counter or sends a notification on every execution needs an operation identity and an explicit effect contract.

WordPress’s next-scheduled-event lookup can help avoid scheduling an event unnecessarily. It is not a lock around the callback and does not prove that a particular business effect will happen only once. Schedule deduplication and execution idempotency solve different problems.

An outbox can durably record that a notification should be delivered alongside a committed state change. Delivery can still repeat if a worker crashes after sending but before recording success. Use destination-side deduplication where available, or document the residual failure window and recovery policy. The integration architecture essay discusses this separation in more detail.

Choose how long the promise lasts

Once a receipt is deleted, the application may treat a very late retry as a new operation. Define the supported retry window and make retention at least consistent with the business and recovery requirements. A payment reference may need a more durable uniqueness rule than a short-lived form-submission token.

Store the minimum useful result, restrict access and avoid persisting passwords, card data or full private response bodies merely to replay a response. An immutable operation outcome can reference a booking whose current state has since changed; decide whether the endpoint returns that original outcome or a current authorised representation, and document the distinction.

Test repetition and uncertainty deliberately

Send the same request twice, then concurrently. Reuse its key with a different quantity. Try the same key from a different account. Interrupt processing before commit, after commit and after an external effect. Retry after receipt expiry and after a permission change.

Assert the number of business operations, the stored result and the final state. Counting successful HTTP responses is insufficient: several responses can legitimately describe the same completed booking.

Idempotency does not promise that every instruction executes once. It lets the application accept repeated attempts while controlling their business effect. That is what turns “try again” from a gamble into a designed recovery path.

Code examples illustrate the architecture and identify application-specific interfaces. Adapt them to your environment and test the failure paths before deployment. Sources are linked alongside the relevant discussion.