Skip to content

Guarantees

This page formally specifies what NestJS RedisX guarantees and what it does not.

Definitions

TermDefinition
At-least-onceOperation may execute multiple times; will execute at least once
At-most-onceOperation executes zero or one time; never more
LeaseTime-bounded exclusive access to a resource
Ownership tokenUnique identifier proving lock ownership
StaleData that may not reflect the latest write

Cache Guarantees

What IS Guaranteed

GuaranteeDescription
Read-your-writesSame process sees its own writes immediately
Bounded stalenessData is at most TTL seconds old
Atomic operationsIndividual get/set/delete are atomic

What is NOT Guaranteed

Not GuaranteedExplanation
Cross-instance consistencyOther instances may see stale data until L1 invalidation propagates
PersistenceCache data may be lost on Redis restart (unless AOF enabled)
OrderingConcurrent writes have undefined order

Consistency Model

Write to Instance A
    ↓ (immediate)
L1 Cache on A updated
    ↓ (immediate)
L2 Redis updated
    ↓ (pub/sub, ~1-10ms)
L1 Cache on B invalidated

Staleness window: 1-10ms for L1 pub/sub propagation

Lock Guarantees

What IS Guaranteed

GuaranteeConditionDescription
Mutual exclusionNormal network, live leaseOnly one holder at a time while TTL is valid or auto-renewal keeps it alive
Ownership verificationAlwaysOnly owner can release or extend (Lua-atomic token check)
Deadlock freedomAlwaysTTL ensures every lock eventually expires

What is NOT Guaranteed

Not GuaranteedExplanationWhat to Use Instead
Consensus-level safetySingle-Redis lock, not Raft/Paxos. Network partition or Redis failover can violate mutual exclusionZooKeeper/etcd for true consensus
Fencing semanticsNo monotonic token for downstream verification. Stale holder can still write after TTL expiresDB constraints + idempotency
Exactly-once executionCrash mid-operation leaves partial state. Lock serializes, doesn't make operations atomicIdempotency plugin + DB constraints
FairnessNo FIFO ordering of waitersApplication-level queuing

Lock Semantics

Acquire(key, ttl):
  IF key not exists:
    SET key = token, EX = ttl
    RETURN token
  ELSE:
    RETURN null (or wait)

Release(key, token):
  IF GET(key) == token:
    DELETE key
  ELSE:
    NO-OP (lost ownership)

Defense-in-Depth for Critical Operations

Locks alone are not enough for payments, financial writes, or any operation where partial execution has consequences:

LayerProtects AgainstAlone Sufficient?
LockConcurrent executionNo — TTL expiry, Redis failover
IdempotencyDuplicate requestsNo — doesn't prevent concurrent first attempts
DB constraint (UNIQUE)Everything aboveYes, but poor UX
All three combinedMost real-world failure modesYes

Idempotency key must be business-meaningful

Use a stable business identifier (orderId, paymentIntentId, requestId) — not a random hash or UUID generated per request. A random key defeats the purpose: every retry looks like a new operation.

Rule of thumb

If losing the lock mid-operation would cost money or corrupt data — add @Idempotent + DB constraints. If it would just cause a retry — lock alone is fine. See Locks → Concepts for code example.

Idempotency Guarantees

What IS Guaranteed

GuaranteeDescription
Duplicate detection (within TTL)Same idempotency key returns cached response
Response replayExact same response for duplicate requests
Concurrent request handlingOne executes, others wait and get same result

What is NOT Guaranteed

Not GuaranteedExplanation
Exactly-once executionFirst request may execute partially before failure
Detection after TTLDuplicates after TTL expiration are processed again
Cross-service detectionIdempotency is per-service unless sharing Redis

Idempotency State Machine

New Request:
  ├─ Key not found → Execute, store response, return
  ├─ Key found (processing) → Wait, return cached response
  └─ Key found (completed) → Return cached response immediately

Rate Limit Guarantees

What IS Guaranteed

GuaranteeDescription
Approximate rate enforcementRequests exceeding limit are rejected
Distributed countingCounts are shared across instances
Sliding window accuracyMore accurate than fixed window

What is NOT Guaranteed

Not GuaranteedExplanation
Exact limit enforcementDistributed systems have inherent race conditions
FairnessNo guaranteed fair distribution among clients
Real-time accuracy5-10% variance is normal

Accuracy Expectations

Configured limit: 100 req/min
Actual enforcement: 95-105 req/min (±5%)

Under high concurrency: 90-110 req/min (±10%)

Streams Guarantees

What IS Guaranteed

GuaranteeDescription
At-least-once deliveryMessages delivered at least once per consumer group
OrderingMessages ordered within a single stream
PersistenceMessages persist until trimmed
Consumer groupsEach message to one consumer per group

What is NOT Guaranteed

Not GuaranteedExplanation
Exactly-once deliveryConsumer may receive same message twice
Cross-stream orderingNo ordering between different streams
Delivery timingBackpressure may delay delivery

Failure Matrix

ScenarioCacheLocksIdempotencyRate LimitStreams
Redis downConfigurable (allow/deny)Fail closedConfigurableConfigurableFail closed
Network partitionStale reads possibleSplit brain riskMay duplicateOver-limit possibleConsumer lag
Process crashData intactAuto-release via TTLData intactData intactRedelivery
Redis restartData lost (no AOF)Locks lostData lostCounters resetData lost (no AOF)

Configuration for Safety Levels

Maximum Safety

typescript
// Cache: Fail if Redis down
new CachePlugin({
  fallback: 'throw',
})

// Locks: No auto-renewal, verify ownership
new LocksPlugin({
  autoRenew: { enabled: false },
  verifyOwnership: true,
})

// Idempotency: Fail if can't check
new IdempotencyPlugin({
  fallback: 'throw',
})

Maximum Availability

typescript
// Cache: Allow on failure
new CachePlugin({
  fallback: 'bypass',
})

// Locks: Auto-renew, short retry
new LocksPlugin({
  autoRenew: { enabled: true },
  retry: { maxRetries: 1 },
})

// Idempotency: Process anyway
new IdempotencyPlugin({
  fallback: 'allow',
})

Approximating Exactly-Once

True exactly-once is impossible in distributed systems. Approximate it with:

typescript
// 1. Idempotency for duplicate detection
@Idempotent()

// 2. Lock for serialization
@WithLock({ key: 'resource:{0}' })

// 3. Database constraint for final safety
// UNIQUE INDEX on idempotency_key

// 4. Idempotent handlers
async process(id: string) {
  // Check if already processed
  if (await this.isProcessed(id)) return;
  
  // Process
  await this.doWork(id);
  
  // Mark processed (idempotent write)
  await this.markProcessed(id);
}

Next Steps

Released under the MIT License.