idempotency-handling — Claude Code skill: setup and features
Implements idempotent API endpoints using Redis, PostgreSQL locks, and idempotency keys. It stops duplicate payments and broken webhooks when retries occur.
npx skills add secondsky/claude-skills --skill idempotency-handling -a claude-codeWhat idempotency-handling does
The skill provides patterns to make API endpoints safe against duplicate calls. It includes Express middleware that stores status codes and responses in Redis for 24 hours under incoming idempotency-key headers.
For database-backed safety, it sets up an idempotency_keys PostgreSQL table. Handlers compute a SHA-256 hash of the payload and run an atomic INSERT ... ON CONFLICT DO NOTHING. If a concurrent call arrives with the identical key, the database rejects the duplicate execution. This stops double spending. Without Redis or a dedicated SQL table, your service cannot guarantee idempotency.
The skill also covers background cleanup. You get hourly batch deletion routines with LIMIT 1000 via SQL, Node.js cron, or Python jobs to prevent table bloat.
What it helps with
- prevent double charging during payment processing retries
- handle incoming webhook duplicates safely
- block race conditions on concurrent API requests
- purge expired idempotency keys in PostgreSQL without table locks
Example prompts for the agent
- Implement Node.js idempotency middleware using Redis with a 24 hour TTL
- Create a PostgreSQL idempotency_keys table and add a handler with payload hash validation
- Set up a scheduled Python cleanup job to remove expired idempotency keys in batches
How to install idempotency-handling
1Skills CLI (Vercel)
npx skills add secondsky/claude-skills --skill idempotency-handling -a claude-code2Manually: put the files in place
~/.claude/skills/idempotency-handling/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/idempotency-handling/
1Skills CLI (Vercel)
npx skills add secondsky/claude-skills --skill idempotency-handling -a codex2Manually: put the files in place
.agents/skills/idempotency-handling/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add secondsky/claude-skills --skill idempotency-handling -a cursor2Manually: put the files in place
.cursor/skills/idempotency-handling/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install secondsky/claude-skills idempotency-handling2Manually: put the files in place
.github/skills/idempotency-handling/SKILL.md1Skills CLI (Vercel)
npx skills add secondsky/claude-skills --skill idempotency-handling -a gemini-cli2Platform CLI
gemini skills install https://github.com/secondsky/claude-skills --path plugins/idempotency-handling/skills/idempotency-handlingManually: put the files in place
.devin/skills/idempotency-handling/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add secondsky/claude-skills --skill idempotency-handling -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about idempotency-handling
How to install idempotency-handling in Claude Code?
Run npx skills add secondsky/claude-skills --skill idempotency-handling -a claude-code in your terminal. The skill will load directly into your agent setup.
What does the idempotency-handling skill do?
It provides architecture patterns for safe retries. The agent receives code templates for Redis caching, PostgreSQL unique constraints, and background cleanup.
How does it handle concurrent duplicate requests?
It relies on an atomic SQL insertion with status processing and ON CONFLICT DO NOTHING. Only the first query executes the operation while parallel calls receive an error.
Source text
SKILL.md · MIT
---
name: idempotency-handling
description: Idempotent API operations with idempotency keys, Redis caching, DB constraints. Use for payment systems, webhook retries, safe retries, or encountering duplicate processing, race conditions, key expiry errors.
license: MIT
---
# Idempotency Handling
Ensure operations produce identical results regardless of execution count.
## Idempotency Key Pattern
```javascript
const redis = require('redis');
const client = redis.createClient();
async function idempotencyMiddleware(req, res, next) {
const key = req.headers['idempotency-key'];
if (!key) return next();
const cached = await client.get(`idempotency:${key}`);
if (cached) {
const { status, body } = JSON.parse(cached);
return res.status(status).json(body);
}
// Store original send
const originalSend = res.json.bind(res);
res.json = async (body) => {
await client.setEx(
`idempotency:${key}`,
86400, // 24 hours
JSON.stringify({ status: res.statusCode, body })
);
return originalSend(body);
};
next();
}
```
## Database-Backed Idempotency
```sql
CREATE TABLE idempotency_keys (
key VARCHAR(255) PRIMARY KEY,
request_hash VARCHAR(64) NOT NULL,
response JSONB,
status VARCHAR(20) DEFAULT 'processing',
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP DEFAULT NOW() + INTERVAL '24 hours'
);
CREATE INDEX idx_idempotency_expires ON idempotency_keys(expires_at);
```
```javascript
async function processPayment(idempotencyKey, payload) {
const requestHash = crypto.createHash('sha256')
.update(JSON.stringify(payload)).digest('hex');
// Try to insert with 'processing' status - only one request will succeed
const insertResult = await db.query(
`INSERT INTO idempotency_keys (key, request_hash, status)
VALUES ($1, $2, 'processing')
ON CONFLICT (key) DO NOTHING
RETURNING *`,
[idempotencyKey, requestHash]
);
// If we inserted the row (rowCount === 1), we're responsible for processing
if (insertResult.rowCount === 1) {
try {
// Execute the payment
const result = await executePayment(payload);
// Update to completed with response
await db.query(
'UPDATE idempotency_keys SET status = $1, response = $2 WHERE key = $3',
['completed', JSON.stringify(result), idempotencyKey]
);
return result;
} catch (error) {
// Mark as failed on error
await db.query(
'UPDATE idempotency_keys SET status = $1, response = $2 WHERE key = $3',
['failed', JSON.stringify({ error: error.message }), idempotencyKey]
);
throw error;
}
}
// Another request is/was processing this key - check status
const existing = await db.query(
'SELECT * FROM idempotency_keys WHERE key = $1',
[idempotencyKey]
);
const row = existing.rows[0];
if (!row) {
throw new Error('Unexpected: idempotency key vanished');
}
// Verify request hasn't changed
if (row.request_hash !== requestHash) {
throw new Error('Idempotency key reused with different request');
}
// Check status
if (row.status === 'completed') {
return JSON.parse(row.response);
} else if (row.status === 'processing') {
throw new Error('Request already processing - retry later');
} else if (row.status === 'failed') {
const failedResponse = JSON.parse(row.response);
throw new Error(`Previous attempt failed: ${failedResponse.error}`);
}
throw new Error(`Unknown status: ${row.status}`);
}
```
## When to Apply
- Payment processing
- Order creation
- Webhook handling
- Email sending
- Any operation where duplicates cause issues
## Best Practices
- Require idempotency keys for mutations
- Validate request body matches stored request
- Set appropriate TTL (24 hours typical)
- Use atomic database operations
- Implement cleanup jobs to prevent table bloat
### TTL Cleanup Strategy
To prevent unbounded table growth, implement periodic cleanup of expired keys:
**Option 1: Scheduled Database Job (PostgreSQL)**
```sql
-- Run hourly via pg_cron or external scheduler
DELETE FROM idempotency_keys
WHERE expires_at < NOW()
LIMIT 1000; -- Batch delete to avoid long locks
```
**Option 2: Application Cleanup Job (Node.js)**
```javascript
// Run via cron or job scheduler (e.g., node-cron, Bull)
async function cleanupExpiredKeys() {
try {
const result = await db.query(
'DELETE FROM idempotency_keys WHERE expires_at < NOW()'
);
console.log(`Cleaned up ${result.rowCount} expired idempotency keys`);
} catch (error) {
console.error('Cleanup job failed:', error);
}
}
// Schedule to run every hour
cron.schedule('0 * * * *', cleanupExpiredKeys);
```
**Option 3: Application Cleanup Job (Python)**
```python
import asyncio
from datetime import datetime
async def cleanup_expired_keys():
"""Remove expired idempotency keys to prevent table bloat."""
try:
result = await db.execute(
"DELETE FROM idempotency_keys WHERE expires_at < $1",
datetime.now()
)
print(f"Cleaned up {result} expired idempotency keys")
except Exception as e:
print(f"Cleanup job failed: {e}")
# Run with APScheduler, Celery, or similar
# scheduler.add_job(cleanup_expired_keys, 'interval', hours=1)
```
**Cleanup Best Practices:**
- Run cleanup during low-traffic periods to minimize lock contention
- Use batched deletes (`LIMIT 1000`) for large tables
- Monitor cleanup job execution and failures
- Consider partitioning the table by created_at for easier cleanup
- Set up alerts if table size grows unexpectedly
© secondsky · MIT · secondsky/claude-skills