Skip to content

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 CounterValueError instead of poisoning aggregates with NaN
  • Testable without Redis - runs on the in-memory testing driver

Installation ​

bash
npm install @nestjs-redisx/core @nestjs-redisx/counters ioredis
bash
npm install @nestjs-redisx/core @nestjs-redisx/counters redis

Quick 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 ​

Released under the MIT License.