Configuration
Full reference for all Idempotency Plugin options.
Basic Configuration
import { RedisModule } from '@nestjs-redisx/core';
import { IdempotencyPlugin } from '@nestjs-redisx/idempotency';
@Module({
imports: [
RedisModule.forRoot({
clients: { host: 'localhost', port: 6379 },
plugins: [
new IdempotencyPlugin({
// Your options here
}),
],
}),
],
})
export class AppModule {}Complete Options Reference
new IdempotencyPlugin({
// Basic Settings
defaultTtl: 86400,
keyPrefix: 'idempotency:',
headerName: 'Idempotency-Key',
// Timeout Settings
lockTimeout: 30000,
waitTimeout: 60000,
// Fingerprinting
validateFingerprint: true,
fingerprintFields: ['method', 'path', 'body'],
fingerprintGenerator: async (context) => {
const req = context.switchToHttp().getRequest();
return createHash('sha256')
.update(`${req.method}|${req.path}|${JSON.stringify(req.body)}`)
.digest('hex');
},
})errorPolicy controls behavior when the store is unavailable
The errorPolicy option decides what happens if the idempotency store (Redis) cannot be reached on the gating checkAndLock call:
'fail-closed'(default) — the request is rejected (the store error propagates). Use this when correctness matters more than availability (e.g. payments).'fail-open'— the request proceeds without idempotency protection (a warning is logged). Use this when availability matters more than deduplication during an outage — and understand the trade: while the store is down, retries execute again (duplicates are possible by design). For money movement keep the default'fail-closed'.
defaultTtl is the deduplication window — not a permanent guarantee
Idempotency records expire after defaultTtl (24h by default). A client that retries the same Idempotency-Key after the record expired is indistinguishable from a new request: the handler executes again, silently. Size defaultTtl to comfortably exceed your clients' longest retry horizon, and for payments treat this plugin as the first line of defense — the durable source of truth for "was this operation performed" belongs in your database (e.g. a unique constraint or a ledger), not in a TTL-bound cache.
Configuration by Use Case
Payment Processing (Strict)
new IdempotencyPlugin({
defaultTtl: 86400, // 24 hours
lockTimeout: 60000, // 1 minute (payments can be slow)
waitTimeout: 120000, // 2 minutes
validateFingerprint: true, // Strict validation
})Order Creation (Standard)
new IdempotencyPlugin({
defaultTtl: 3600, // 1 hour
lockTimeout: 30000, // 30 seconds
waitTimeout: 60000, // 1 minute
validateFingerprint: true,
})Webhook Handling (Lenient)
new IdempotencyPlugin({
defaultTtl: 86400,
headerName: 'X-Webhook-ID', // Custom header
validateFingerprint: false, // Body may vary
})
// Note: requests without an idempotency key are simply passed through
// (the interceptor skips them) — there is no "require key" enforcement.
// errorPolicy: 'fail-open' can be added here to keep webhooks flowing
// even if Redis is temporarily unavailable.Async Configuration with registerAsync
For type-safe configuration via NestJS DI:
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RedisModule } from '@nestjs-redisx/core';
import { IdempotencyPlugin } from '@nestjs-redisx/idempotency';
@Module({
imports: [
RedisModule.forRoot({
clients: { host: 'localhost', port: 6379 },
plugins: [
IdempotencyPlugin.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
defaultTtl: config.get('IDEMPOTENCY_TTL', 86400),
headerName: config.get('IDEMPOTENCY_HEADER', 'Idempotency-Key'),
lockTimeout: config.get('IDEMPOTENCY_LOCK_TIMEOUT', 30000),
waitTimeout: config.get('IDEMPOTENCY_WAIT_TIMEOUT', 60000),
validateFingerprint: config.get('IDEMPOTENCY_VALIDATE_FP', true),
}),
}),
],
}),
],
})
export class AppModule {}Environment Configuration
// config/idempotency.config.ts
import { registerAs } from '@nestjs/config';
export default registerAs('idempotency', () => ({
ttl: parseInt(process.env.IDEMPOTENCY_TTL || '86400', 10),
headerName: process.env.IDEMPOTENCY_HEADER || 'Idempotency-Key',
keyPrefix: process.env.IDEMPOTENCY_PREFIX || 'idempotency:',
lockTimeout: parseInt(process.env.IDEMPOTENCY_LOCK_TIMEOUT || '30000', 10),
waitTimeout: parseInt(process.env.IDEMPOTENCY_WAIT_TIMEOUT || '60000', 10),
}));# .env
IDEMPOTENCY_TTL=86400
IDEMPOTENCY_HEADER=Idempotency-Key
IDEMPOTENCY_LOCK_TIMEOUT=30000
IDEMPOTENCY_WAIT_TIMEOUT=60000Options Deep Dive
TTL Settings
| Option | Type | Default | Description |
|---|---|---|---|
defaultTtl | number | 86400 | Record lifetime (seconds) |
TTL Guidelines:
| Operation Type | Recommended TTL | Reason |
|---|---|---|
| Payments | 24-48 hours | Important, may retry next day |
| Orders | 1-24 hours | Session-based |
| Notifications | 1-4 hours | Time-sensitive |
| Webhooks | 24-72 hours | May replay |
Timeout Settings
| Option | Type | Default | Description |
|---|---|---|---|
lockTimeout | number | 30000 | Max processing time (ms) |
waitTimeout | number | 60000 | Max wait for concurrent (ms) |
Timeout Relationship:
waitTimeout >= lockTimeout + safety_margin
Recommended: waitTimeout = lockTimeout * 2Fingerprint Settings
| Option | Type | Default | Description |
|---|---|---|---|
validateFingerprint | boolean | true | Check request matches |
fingerprintFields | array | ['method', 'path', 'body'] | Fields to hash |
fingerprintGenerator | function | undefined | Custom hash function |
Next Steps
- Decorator — Learn @Idempotent decorator
- Fingerprinting — Deep dive into fingerprints