(+351) 21 24 10006  ·  info@bconcepts.pt
Carnaxide, Lisbon

How to maintain per-user counters in Real-Time Analytics

João Barros 29 de August de 2026 4 min read

This tutorial shows how to maintain per-user counters in Real-Time Analytics, useful for metrics such as events per session, limits per minute or real-time quotas. You will learn how to update incremental counters, handle simple duplicates and expire old counters to control state.

Prerequisites

  • Account and access to a Real-Time Analytics or streaming service (for example, Azure Stream Analytics, Kusto/ADX or another with support for stateful processing).
  • Event source with fields: userId, eventId, eventTime.
  • Basic knowledge of SQL/KQL or the query language of the service used.

Step 1: Define the granularity and the state you want to maintain

Decide whether the counter will be per minute, per hour, per session or cumulative. This determines the state key and the expiration rules. For example: counter per userId per minute. The key will be (userId, minuteBucket).

Step 2: Normalize events and compute the time bucket

Convert eventTime to a bucket (e.g., minute) to group events in the same interval. This avoids issues with time zones and microseconds.

// Exemplo KQL para transformar timestamp em bucket por minuto
Events
| extend minuteBucket = startofminute(eventTime)
| project userId, eventId, minuteBucket, eventTime

Step 3: Simple deduplication by eventId before updating state

A basic deduplication by eventId prevents duplicate counts when events are redelivered. Keep a small cache of eventId per bucket (or use a dedupe function of the service).

// Exemplo conceptual: manter apenas o primeiro eventId por user/minute
Events
| extend minuteBucket = startofminute(eventTime)
| summarize firstEventTime = min(eventTime) by userId, minuteBucket, eventId
| summarize eventsCount = count() by userId, minuteBucket

Step 4: Update state with atomic operation (incremental)

Use an atomic operation in the state store (Redis, Cosmos DB, Azure Table, the engine's state store) to add the number of events per key. This avoids race conditions when multiple workers process the same userId.

// Pseudocódigo conceptual para cada (userId, minuteBucket, delta)
stateKey = userId + ':' + minuteBucket
current = stateStore.get(stateKey) // pode ser 0 se não existir
newValue = current + delta
stateStore.set(stateKey, newValue, ttl=120s) // define TTL para expirar

Step 5: Manage expiration and state cleanup

Set an appropriate TTL (time-to-live) for the buckets: for example, keep counters for 2-3 extra windows to allow reprocessing. Automatic expiration frees memory and prevents permanent counters for inactive users.

// Exemplo de TTL ao escrever em Redis (com comandos Redis simples)
// stateKey = "user:123:2026-08-29T12:34"
INCRBY stateKey 5
EXPIRE stateKey 180  // 180 segundos de TTL

Step 6: Combine counters for reporting (hourly example)

To obtain hourly metrics combine the minuteBuckets within the hour and sum the values from the state store. You can do this in a periodic batch or in a query that aggregates the relevant keys.

// Pseudocódigo para agregar 60 buckets de um user numa hora
hourBuckets = keysMatching("user:123:2026-08-29T12:*")
sum = 0
for k in hourBuckets:
    sum += get(k)
return sum

Verify the outcome

Validate behavior with these tests: 1) Send 10 unique events for the same user in the same minute and confirm the counter increases to 10. 2) Resend a subset of the same eventId and confirm the counter does not duplicate. 3) Wait until the TTL expires and verify that the state is removed. Use logs and queries to the state store to confirm values per key.

Conclusion

By maintaining per-user counters with time buckets, deduplication by eventId, atomic operations and controlled TTL, you achieve stable and efficient real-time metrics. Next steps: implement sliding windows, handle event reordering and use an exactly-once mechanism when needed. Tip: start with a generous TTL and reduce it as tests prove behavior.