Enterprise software licenses are often based on named users. Sometimes, however, the license agreement is based on concurrent users.
That sounds like a much simpler number than it actually is.
If a vendor asks how many users were concurrently using a system over the last 30 days, what exactly should we count?
Counting logins is clearly wrong. Counting users who performed transactions during the same hour is also wrong. Looking at currently connected sessions only tells us what is happening now. And relying on logout records assumes that every session ends cleanly—which is often not how real enterprise systems behave.
We recently needed to solve this problem for a Blue Yonder WMS environment. The resulting approach is useful beyond Blue Yonder because the underlying problem is the same for any application where licensing depends on concurrent usage.
You can download the script here.
First, Define “Concurrent”
Two users are concurrent if their active sessions overlap in time.
Suppose:
user a: 08:05 – 08:50
user b: 08:20 – 09:10
user c: 08:35 – 08:45
At 08:10, concurrency is 1.
At 08:25, it is 2.
At 08:40, it is 3.
So the answer for the 08:00 hour is not the number of users who used the application during that hour. All three did.
The useful number is:
Peak concurrency during the hour = 3
That distinction matters. Counting distinct users who were active at some point during an hour measures usage, not concurrency.
Now Define a Session
The next problem is harder.
A successful login gives us a very good session start:
session_start = successful login timestamp
A logout would appear to give us the end.
Unfortunately, real systems do not always produce a corresponding logout. A workstation can disappear. A client can terminate unexpectedly. A connection can be interrupted. There are many ways for a session to start without leaving behind the logout event we would ideally like to see.
We therefore need a deterministic definition of session end.
For our implementation, a session ends at the earliest of:
- The next login or logout event for the same user.
- A configurable maximum session duration.
- The current time, if the session is still within that maximum duration.
We use eight hours as the default maximum, but deliberately make it configurable.
The important point is not that eight hours is universally correct. It is that an audit methodology needs an explicit, reproducible rule rather than allowing a missing logout to create an indefinitely active user.
We define the resulting session as a half-open interval:
[login_time, session_end)
The user is active at the instant of login and inactive at the exact session-end timestamp.
This avoids artificial overlap when one session ends at exactly the same instant another begins.
Why Not Use Transaction Activity?
Another tempting approach is to infer activity from the user’s last transaction.
That creates a different problem.
A user can be legitimately using the application without generating the type of business transaction we happen to be monitoring. They may be querying data, reviewing screens, investigating an order, or simply spending time between actions.
A transaction timestamp tells us that a user did something at that instant.
It does not reliably tell us when their authenticated session ended.
For a concurrent-user audit, session evidence is therefore a better foundation than business transaction activity.
We Don’t Need to Check Every Second
Once sessions are defined, the brute-force solution would be to calculate active users every second—or perhaps every minute.
That isn’t necessary.
Concurrency can increase only when a session starts.
When somebody logs out, concurrency can only decrease. When an inferred session reaches its maximum duration, concurrency can only decrease.
Therefore, within each hour we only need to evaluate concurrency at:
the beginning of the hour
+
every session start within the hour
At each of those points we count the distinct users whose sessions contain that instant.
Then:
hourly_peak =
max(concurrent_users at each evaluation point)
This gives us exact peak concurrency for the hour without continuously sampling the system.
Why the Hour Boundary Matters
Consider a user who logs in at 07:45 and remains logged in until 08:30.
If we looked only at login events occurring between 08:00 and 09:00, we would miss that user until somebody else logged in.
That is why every hour begins with an evaluation point at exactly the hour boundary.
At 08:00 we ask:
which sessions started on or before 08:00
and end after 08:00?
Then we repeat the calculation at every new login occurring during that hour.
The highest result is the peak for that hour.
Multiple Instances Make It More Interesting
Enterprise applications frequently have multiple instances.
Suppose Instance A has two active users:
a1: 08:05 – 08:55
a2: 08:10 – 08:50
Instance B then gets another login:
b1: 08:20 – 08:40
The enterprise concurrency at 08:20 is 3.
There is a subtle trap here.
If each instance independently calculates concurrency only at its own login timestamps and we combine the results afterward, Instance A never evaluates itself at 08:20. That timestamp originated on Instance B.
We can therefore understate the true enterprise peak.
The correct sequence is:
derive sessions on every instance
|
normalize timestamps
|
combine sessions and candidate points
|
evaluate every session against every global candidate point
|
count distinct active users
|
take the maximum for each hour
For our implementation, timestamps from the individual WMS instances are normalized to UTC before global concurrency is calculated.
This also gives us two useful views:
Local concurrency tells us the peak on each individual application instance in that instance’s local time.
Global concurrency tells us the peak number of distinct users across the enterprise in UTC.
If the same user is simultaneously connected to two instances, the global calculation still counts that user once. We are measuring concurrent users, not concurrent sessions.
The Algorithm
At a high level, the implementation looks like this:
for each application instance in parallel:
read login and logout audit events
order events by user and timestamp
for each successful login:
session_start = login timestamp
session_end = earliest of:
next login/logout event
maximum configured session duration
current time
determine which sessions are eligible for the audit
generate local evaluation points:
each hour boundary
each login timestamp
convert session times to utc
generate global evaluation points:
each utc hour boundary
each login timestamp in utc
combine data from all instances
for local reporting:
for each instance and evaluation point:
count distinct users where:
session_start <= evaluation_point
and session_end > evaluation_point
for each instance and hour:
peak = maximum concurrency
for global reporting:
combine evaluation points from every instance
for each global evaluation point:
count distinct users across all instances where:
session_start_utc <= evaluation_point
and session_end_utc > evaluation_point
for each utc hour:
peak = maximum concurrency
There is no sampling approximation in the concurrency calculation. Given the session boundaries we have defined, evaluating the hour boundary and session starts is sufficient to find the maximum.
The only inference is the explicitly documented rule used to determine the end of a session when a definitive logout is unavailable.
Making the Assumptions Visible
For something that may be used in a license audit, I think this is particularly important.
The goal should not be to hide assumptions inside code.
The opposite is better.
Expose them.
Our implementation therefore makes parameters such as these explicit:
maximum inferred session duration
audit start date/time
login signature
logout signature
whether system users are counted
local vs. global reporting
If eight hours is challenged as the maximum session duration, change it and rerun the analysis.
If system/service accounts should not count toward the license, exclude them according to an agreed definition.
The calculation remains deterministic and reproducible.
That is much more defensible than producing a number whose underlying definition of “concurrent user” nobody can quite explain.
The Blue Yonder Implementation
We implemented this methodology for Blue Yonder WMS using MOCA and the WMS audit data.
The implementation executes against multiple WMS instances in parallel, derives the session intervals on each instance, normalizes timestamps for enterprise-level analysis, combines the resulting data, and calculates both local and global hourly peaks.
The complete script is attached here.
It deliberately exposes the assumptions that affect the result rather than burying them in the implementation. That makes the script useful not only for producing the concurrent-user count, but also for explaining exactly how that number was derived.
And for a software license audit, that second part may be just as important as the number itself.
Saad Ahmad
saad.ahmad@smart-is.com