How to maintain per-user counters in Real-Time Analytics
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.