How Idempotency Saves Your API from Chaos
June 11, 2025
Understanding API Idempotency: A Practical Guide
Idempotency is a crucial concept in API design that ensures:
Making the same request multiple times produces the same result without side effects.
This means that if a client sends the same request twice (or more), the system should handle it gracefully without creating duplicate records or causing unintended consequences. Think of it like a light switch: pressing it multiple times doesn't create multiple lights—it just toggles the same light on or off.
Why Idempotency Matters
The Double-Charge Problem
Consider this common e-commerce scenario:
- A user clicks "Pay $100" on a mobile app
- The network connection is unstable
- The user doesn't see a response
- They click again
- Result: The user gets charged $200 instead of $100
This is a real problem that affects user trust and can lead to:
- Customer support tickets
- Refund requests
- Loss of customer trust
- Potential legal issues
The Reload Problem
Another common issue occurs in booking systems:
- User fills out a hotel booking form
- Page generates a new
bookingId = "booking_123"
- User clicks "Confirm Booking" but the request fails
- User reloads the page
- A new
bookingId = "booking_456"
is generated - User clicks "Confirm Booking" again
- Result: Two separate bookings are created for the same room and dates
Implementing Idempotency
Client-Side Implementation
The key is to generate and persist a stable ID that represents the user's intent. Here's a React hook that handles this:
// useIdempotentAction.ts
import { useState, useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
interface UseIdempotentActionProps {
actionKey: string; // e.g., 'booking', 'payment', 'subscription'
onSuccess?: () => void;
}
export function useIdempotentAction({ actionKey, onSuccess }: UseIdempotentActionProps) {
const [actionId, setActionId] = useState<string>('');
const storageKey = `${actionKey}-id`;
useEffect(() => {
// Try to retrieve existing action ID
const saved = localStorage.getItem(storageKey);
if (saved) {
setActionId(saved);
} else {
// Generate new ID if none exists
const newId = uuidv4();
localStorage.setItem(storageKey, newId);
setActionId(newId);
}
}, [storageKey]);
const clearActionId = () => {
localStorage.removeItem(storageKey);
setActionId('');
onSuccess?.();
};
return { actionId, clearActionId };
}
Usage in a booking component:
// BookingForm.tsx
function BookingForm() {
const { actionId, clearActionId } = useIdempotentAction({
actionKey: 'booking',
onSuccess: () => {
// Show success message
// Redirect to confirmation page
}
});
const handleSubmit = async (formData: BookingFormData) => {
try {
const response = await fetch('/api/bookings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': actionId
},
body: JSON.stringify({
...formData,
idempotencyKey: actionId
})
});
if (response.ok) {
clearActionId();
}
} catch (error) {
// Handle error
}
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
</form>
);
}
Why Client-Side ID Generation?
The client is the best place to generate the ID because:
- Intent Tracking: It knows the exact moment of user intent
- State Persistence: It can persist the ID across page reloads
- Retry Management: It can track retry attempts
- Industry Standard: It follows the pattern used by industry leaders like Stripe and PayPal
Practical Implementation with Supabase
Client-Side Code
// api/bookings.ts
interface BookingRequest {
roomId: string;
checkIn: string;
checkOut: string;
guestInfo: {
name: string;
email: string;
};
}
async function createBooking(bookingData: BookingRequest, idempotencyKey: string) {
const response = await fetch('/api/bookings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify({
...bookingData,
idempotencyKey
})
});
if (!response.ok) {
throw new Error('Booking failed');
}
return response.json();
}
Server-Side Code
// pages/api/bookings.ts
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_KEY!
);
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { idempotencyKey, ...bookingData } = req.body;
try {
// First, check if we've already processed this request
const { data: existingBooking } = await supabase
.from('bookings')
.select('*')
.eq('idempotency_key', idempotencyKey)
.single();
if (existingBooking) {
// Return the existing booking
return res.status(200).json(existingBooking);
}
// Process new booking
const { data, error } = await supabase
.from('bookings')
.insert({
...bookingData,
idempotency_key: idempotencyKey,
status: 'confirmed',
created_at: new Date().toISOString()
})
.select()
.single();
if (error) {
throw error;
}
return res.status(201).json(data);
} catch (error) {
console.error('Booking error:', error);
return res.status(500).json({ error: 'Booking failed' });
}
}
Best Practices for Idempotency
-
Generate Stable IDs
- Use UUIDs (v4) for uniqueness
- Include a prefix for different action types (e.g.,
booking_
,payment_
) - Store them in localStorage, cookies, or session storage
- Clear them after confirmed success
-
Handle Edge Cases
- Network failures and timeouts
- Page reloads and browser crashes
- Multiple clicks and rapid retries
- Concurrent requests
-
Server-Side Considerations
- Use database constraints and unique indexes
- Implement proper error handling and logging
- Set appropriate timeouts for idempotency keys
- Consider using a distributed cache (Redis) for high-traffic systems
-
Security Considerations
- Validate idempotency keys
- Set expiration times for keys
- Implement rate limiting
- Log suspicious patterns
Summary
Idempotency is not about preventing duplicate requests—it's about handling them gracefully. A well-designed system should:
- Accept the same request multiple times
- Process it only once
- Return the same result each time
- Maintain data consistency
- Handle errors gracefully
Remember:
Your job isn't to block duplicates.
Your job is to make sure they don't hurt anyone.
By implementing proper idempotency, you create a more resilient system that can handle real-world scenarios like:
- Poor network conditions
- User impatience
- Browser refreshes
- Mobile app backgrounding
- Service interruptions
This leads to:
- Better user experience
- Fewer support tickets
- More reliable systems
- Happier customers
Recent posts
- At-Least-Once vs. Exactly-Once - Understanding Message Delivery Guarantees
June 12, 2025
Learn about message delivery guarantees in distributed systems. Understand why most production systems implement at-least-once delivery with idempotency rather than attempting exactly-once delivery.
- How Idempotency Saves Your API from Chaos
June 11, 2025
Learn how to implement idempotency in your APIs to prevent duplicate actions and ensure data consistency. Includes practical examples with Supabase and Node.js.
- Vibe Coding ‑ Notes from the First Try
June 6, 2025
Quick lessons from spinning up a new blog with an AI pair‑programmer and Cursor.