Counters Plugin
Fleet-wide counters that never lose updates, windows that clean themselves up, and monotonic sequences.
Overview
this.views++ loses updates the moment you run two replicas. Redis INCR is the canonical fix - @nestjs-redisx/counters wraps it with validated inputs, window TTL semantics that actually behave (the first write opens the window, later writes never extend it), atomic read-and-reset for batch flushes, and loud failures on poisoned keys instead of silent NaN.
Key Features
- Atomic everywhere - every mutation is one Redis operation; concurrent increments from any number of instances all count
- Window counters -
increment(key, 1, { ttl })applies the TTL only when the increment creates the key:api:${userId}:${today}expires on its own getAndReset- read the accumulated total and zero it in one atomic step- Sequences -
next('seq:invoices')issues 1, 2, 3… across the whole fleet - Strict by design - amounts must be non-zero safe integers; a non-numeric key throws
CounterValueErrorinstead of poisoning aggregates withNaN - Testable without Redis - runs on the in-memory testing driver
Installation
bash
npm install @nestjs-redisx/core @nestjs-redisx/counters ioredisbash
npm install @nestjs-redisx/core @nestjs-redisx/counters redisQuick Start
typescript
import { Module } from '@nestjs/common';
import { RedisModule } from '@nestjs-redisx/core';
import { CountersPlugin } from '@nestjs-redisx/counters';
@Module({
imports: [
RedisModule.forRoot({
clients: {
host: 'localhost',
port: 6379,
},
plugins: [
new CountersPlugin({
keyPrefix: '_ctr:', // default
}),
],
}),
],
})
export class AppModule {}typescript
import { Injectable, Inject } from '@nestjs/common';
import { COUNTER_SERVICE, ICounterService } from '@nestjs-redisx/counters';
@Injectable()
export class StatsService {
constructor(
@Inject(COUNTER_SERVICE)
private readonly counters: ICounterService,
) {}
// Atomic fleet-wide increment - concurrent updates never get lost.
async onArticleView(articleId: string): Promise<number> {
return this.counters.increment(`views:${articleId}`);
}
async getViews(articleId: string): Promise<number> {
return this.counters.get(`views:${articleId}`); // 0 when absent
}
// Counters go negative freely - clamp in domain code if needed.
async releaseSeat(eventId: string): Promise<number> {
return this.counters.decrement(`seats:${eventId}`);
}
async resetViews(articleId: string): Promise<boolean> {
return this.counters.reset(`views:${articleId}`);
}
}When NOT to Use Counters
- Enforcing request rates - that's the rate limiter (token bucket / sliding window are more than a counter).
- Bounding concurrency - that's the semaphore.
- Prometheus metrics - counters here are domain state (quotas, stock, sequences); operational telemetry belongs to the metrics plugin.
Next Steps
- Configuration
- Service API
- Recipes - usage quotas, batch flushes, live stats, ID generation
- Testing
- Troubleshooting