api-design api idempotencyidempotent requestsapi reliability

API Idempotency: Complete Implementation Patterns Guide

Master API idempotency patterns for reliable systems. Learn implementation strategies, real-world examples, and best practices for idempotent requests.

📖 16 min read 📅 April 12, 2026 ✍ By PropTechUSA AI
16m
Read Time
3.2k
Words
18
Sections

Building resilient APIs requires more than just handling happy path scenarios. When network timeouts, connection drops, and retry mechanisms come into play, your [API](/workers) needs to handle duplicate requests gracefully. This is where API idempotency becomes critical – ensuring that performing the same operation multiple times produces the same result as performing it once.

Understanding API Idempotency Fundamentals

What Makes an Operation Idempotent

API idempotency means that making multiple identical requests has the same effect as making a single request. This concept is crucial for building reliable distributed systems where network failures and client retries are common occurrences.

Consider a payment processing scenario: if a client sends a payment request and doesn't receive a response due to a network timeout, they shouldn't hesitate to retry. Without proper idempotency controls, this could result in duplicate charges – a critical business problem.

HTTP Methods and Natural Idempotency

Some HTTP methods are naturally idempotent:

However, POST requests are typically not idempotent, as they often create new resources or trigger side effects. This is where explicit idempotency implementation becomes essential.

The Business Impact of Non-Idempotent APIs

At PropTechUSA.ai, we've seen how critical idempotency is in [property](/offer-check) management systems. When a tenant payment fails to receive confirmation, retry attempts without idempotency controls can lead to:

Core Idempotency Implementation Patterns

Idempotency Keys Pattern

The most robust approach involves using client-generated idempotency keys. Clients include a unique identifier with each request, and the server uses this key to detect and handle duplicates.

typescript
interface IdempotentRequest {

idempotencyKey: string;

operation: string;

payload: any;

timestamp: number;

}

class IdempotencyService {

private cache: Map<string, any> = new Map();

async processRequest(request: IdempotentRequest): Promise<any> {

const cacheKey = ${request.operation}:${request.idempotencyKey};

// Check if we've seen this request before

if (this.cache.has(cacheKey)) {

const cachedResult = this.cache.get(cacheKey);

// Verify payload consistency

if (!this.payloadMatches(cachedResult.originalPayload, request.payload)) {

throw new Error('Payload mismatch for idempotency key');

}

return cachedResult.response;

}

// Process the request for the first time

const result = await this.executeOperation(request);

// Cache the result

this.cache.set(cacheKey, {

response: result,

originalPayload: request.payload,

processedAt: new Date()

});

return result;

}

private payloadMatches(cached: any, current: any): boolean {

return JSON.stringify(cached) === JSON.stringify(current);

}

}

Database-Backed Idempotency

For production systems, implement idempotency tracking in your database to ensure persistence across service restarts:

sql
CREATE TABLE idempotency_records (

idempotency_key VARCHAR(255) PRIMARY KEY,

operation_type VARCHAR(100) NOT NULL,

request_hash VARCHAR(64) NOT NULL,

response_data JSONB,

status VARCHAR(20) DEFAULT 'processing',

created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

expires_at TIMESTAMP

);

CREATE INDEX idx_idempotency_expires ON idempotency_records(expires_at);

typescript
class DatabaseIdempotencyService {

async processIdempotentRequest(

idempotencyKey: string,

operation: () => Promise<any>,

requestPayload: any,

ttlMinutes: number = 1440

): Promise<any> {

const requestHash = this.hashPayload(requestPayload);

const expiresAt = new Date(Date.now() + ttlMinutes * 60000);

// Attempt to insert idempotency record

try {

await this.db.query(

INSERT INTO idempotency_records

(idempotency_key, operation_type, request_hash, expires_at)

VALUES ($1, $2, $3, $4)

, [idempotencyKey, 'payment', requestHash, expiresAt]);

// First time seeing this key - process the operation

const result = await operation();

// Update with successful result

await this.db.query(

UPDATE idempotency_records

SET response_data = $1, status = 'completed'

WHERE idempotency_key = $2

, [JSON.stringify(result), idempotencyKey]);

return result;

} catch (error) {

if (error.code === '23505') { // Unique constraint violation

// Key already exists - check for payload consistency

const existing = await this.db.query(

SELECT request_hash, response_data, status

FROM idempotency_records

WHERE idempotency_key = $1

, [idempotencyKey]);

if (existing.rows[0].request_hash !== requestHash) {

throw new Error('Request payload mismatch for idempotency key');

}

if (existing.rows[0].status === 'completed') {

return JSON.parse(existing.rows[0].response_data);

}

// Request is still processing - return appropriate response

throw new Error('Request already in progress');

}

throw error;

}

}

private hashPayload(payload: any): string {

return crypto

.createHash('sha256')

.update(JSON.stringify(payload))

.digest('hex');

}

}

Conditional Requests Pattern

For update operations, use ETags or version numbers to implement conditional requests:

typescript
class PropertyUpdateService {

async updateProperty(

propertyId: string,

updates: Partial<Property>,

ifMatch?: string

): Promise<Property> {

const current = await this.getProperty(propertyId);

if (ifMatch && current.etag !== ifMatch) {

throw new Error('Property has been modified by another request');

}

const updated = {

...current,

...updates,

updatedAt: new Date(),

version: current.version + 1

};

updated.etag = this.generateETag(updated);

await this.saveProperty(updated);

return updated;

}

private generateETag(property: Property): string {

return crypto

.createHash('md5')

.update(${property.id}-${property.version}-${property.updatedAt})

.digest('hex');

}

}

Advanced Implementation Strategies

Handling Long-Running Operations

For operations that take significant time to complete, implement asynchronous idempotency with status tracking:

typescript
class AsyncIdempotentProcessor {

async initiateOperation(

idempotencyKey: string,

operationType: string,

payload: any

): Promise<{ operationId: string; status: string }> {

// Check for existing operation

const existing = await this.findOperation(idempotencyKey);

if (existing) {

return {

operationId: existing.id,

status: existing.status

};

}

// Create new operation record

const operation = await this.createOperation({

idempotencyKey,

operationType,

payload,

status: 'pending'

});

// Queue for background processing

await this.queueProcessor.enqueue({

operationId: operation.id,

type: operationType,

payload

});

return {

operationId: operation.id,

status: 'pending'

};

}

async getOperationStatus(operationId: string): Promise<OperationStatus> {

const operation = await this.findOperationById(operationId);

return {

id: operation.id,

status: operation.status,

result: operation.result,

error: operation.error,

createdAt: operation.createdAt,

completedAt: operation.completedAt

};

}

}

Distributed Idempotency with Redis

For microservices architectures, use Redis for shared idempotency state:

typescript
class RedisIdempotencyService {

constructor(private redis: Redis) {}

async processWithIdempotency<T>(

key: string,

operation: () => Promise<T>,

ttlSeconds: number = 3600

): Promise<T> {

const lockKey = idempotency:lock:${key};

const resultKey = idempotency:result:${key};

// Try to acquire lock

const lockAcquired = await this.redis.set(

lockKey,

'processing',

'PX',

30000, // 30 second lock timeout

'NX'

);

if (!lockAcquired) {

// Check if result exists

const existingResult = await this.redis.get(resultKey);

if (existingResult) {

return JSON.parse(existingResult);

}

// Wait and retry

await new Promise(resolve => setTimeout(resolve, 100));

return this.processWithIdempotency(key, operation, ttlSeconds);

}

try {

// Check once more for existing result

const existingResult = await this.redis.get(resultKey);

if (existingResult) {

return JSON.parse(existingResult);

}

// Execute operation

const result = await operation();

// Store result

await this.redis.setex(

resultKey,

ttlSeconds,

JSON.stringify(result)

);

return result;

} finally {

// Release lock

await this.redis.del(lockKey);

}

}

}

Error Handling and Recovery

Implement robust error handling for partial failures:

typescript
class ResilientIdempotentService {

async processPayment(

idempotencyKey: string,

paymentData: PaymentRequest

): Promise<PaymentResult> {

const operation = async () => {

// Step 1: Validate payment

const validation = await this.validatePayment(paymentData);

if (!validation.valid) {

throw new ValidationError(validation.errors);

}

// Step 2: Reserve funds

const reservation = await this.reserveFunds(paymentData);

try {

// Step 3: Process payment

const payment = await this.processPaymentInternal(paymentData);

// Step 4: Confirm reservation

await this.confirmReservation(reservation.id);

return {

paymentId: payment.id,

status: 'completed',

amount: payment.amount

};

} catch (error) {

// Rollback reservation on payment failure

await this.releaseReservation(reservation.id);

throw error;

}

};

return this.idempotencyService.processWithIdempotency(

idempotencyKey,

operation

);

}

}

⚠️
WarningAlways implement proper cleanup mechanisms for failed operations to prevent resource leaks and inconsistent states.

Production Best Practices and Performance

Idempotency Key Generation Guidelines

Establish clear guidelines for idempotency key generation:

typescript
class IdempotencyKeyGenerator {

// For user-initiated actions

static forUserAction(

userId: string,

action: string,

timestamp: number

): string {

return user:${userId}:${action}:${timestamp};

}

// For scheduled operations

static forScheduledTask(

taskType: string,

scheduledTime: Date

): string {

const timeString = scheduledTime.toISOString().slice(0, 16); // minute precision

return scheduled:${taskType}:${timeString};

}

// For external system integration

static forExternalEvent(

systemId: string,

eventId: string

): string {

return external:${systemId}:${eventId};

}

}

Performance Optimization Strategies

Optimize idempotency checking for high-throughput APIs:

typescript
class OptimizedIdempotencyService {

private cache = new LRUCache<string, any>({

max: 10000,

ttl: 1000 * 60 * 15 // 15 minutes

});

async checkIdempotency(

key: string,

payload: any

): Promise<{ exists: boolean; result?: any }> {

// Fast path: check in-memory cache first

const cached = this.cache.get(key);

if (cached) {

if (this.payloadMatches(cached.payload, payload)) {

return { exists: true, result: cached.result };

} else {

throw new Error('Payload mismatch for cached idempotency key');

}

}

// Fallback to database check

const dbResult = await this.checkDatabase(key, payload);

if (dbResult.exists) {

// Cache for future requests

this.cache.set(key, {

payload,

result: dbResult.result

});

}

return dbResult;

}

}

Monitoring and Observability

Implement comprehensive monitoring for idempotency patterns:

typescript
class IdempotencyMetrics {

private [metrics](/dashboards) = {

duplicateRequests: new Counter({

name: 'idempotent_duplicate_requests_total',

help: 'Total number of duplicate requests detected'

}),

payloadMismatches: new Counter({

name: 'idempotent_payload_mismatches_total',

help: 'Total number of payload mismatches for same idempotency key'

}),

cacheHitRate: new Histogram({

name: 'idempotent_cache_hit_rate',

help: 'Cache hit rate for idempotency checks'

})

};

recordDuplicateRequest(operationType: string): void {

this.metrics.duplicateRequests.inc({ operation: operationType });

}

recordPayloadMismatch(operationType: string): void {

this.metrics.payloadMismatches.inc({ operation: operationType });

}

}

💡
Pro TipMonitor your idempotency hit rates to understand client retry patterns and optimize cache sizing accordingly.

Cleanup and Maintenance

Implement automated cleanup for expired idempotency records:

typescript
class IdempotencyMaintenance {

async cleanupExpiredRecords(): Promise<void> {

const batchSize = 1000;

let deletedCount = 0;

do {

const result = await this.db.query(

DELETE FROM idempotency_records

WHERE expires_at < NOW()

AND id IN (

SELECT id FROM idempotency_records

WHERE expires_at < NOW()

LIMIT $1

)

, [batchSize]);

deletedCount = result.rowCount;

// Pause between batches to avoid overwhelming the database

if (deletedCount === batchSize) {

await new Promise(resolve => setTimeout(resolve, 100));

}

} while (deletedCount === batchSize);

}

}

Building Resilient APIs with Idempotency

Implementing robust API idempotency requires careful consideration of your specific use cases, performance requirements, and failure scenarios. The patterns covered here provide a foundation for building reliable systems that handle duplicate requests gracefully.

At PropTechUSA.ai, we've implemented these idempotency patterns across our property management platform, ensuring that critical operations like lease processing, payment handling, and maintenance scheduling remain consistent even under adverse network conditions.

Start by identifying your most critical API endpoints – those handling financial transactions, state changes, or external integrations. Implement idempotency incrementally, beginning with database-backed solutions for reliability, then optimizing with caching layers for performance.

Remember that idempotency is not just about preventing duplicate operations; it's about building trust with your API consumers and ensuring data consistency across distributed systems.

Ready to implement bulletproof idempotency in your APIs? Begin with the database-backed approach for your most critical endpoints, and gradually expand to cover your entire API surface. Your future self – and your users – will thank you when those inevitable network hiccups occur.

🚀 Ready to Build?

Let's discuss how we can help with your project.

Start Your Project →