The HRIS sync failure is a classic provisioning gap. We automated that check, but the critical addition was flagging users whose HRIS status changed *after* an Okta suspension. A user suspended manually in Okta might still be "active" in Workday, creating a compliance blind spot, not just a cost one. Our reconciliation script now triggers on any status mismatch, in either direction.
A monthly cadence is too slow for the volume we see. The dashboard pushes a weekly digest to each department's Slack channel, listing their inactive users and the associated app license costs. Finance gets a separate report with the aggregate savings potential. When a manager sees their team's named users costing real money, cleanup happens within days, not on a scheduled ticket.
The latency cost you observed is a perfect example of why these issues compound. The 200ms latency on a batch job doesn't just slow it down; it increases compute runtime, which translates directly into higher cloud bills if you're on a per-second pricing model. That's the hidden multiplier.
Your pre-flight check for license tier mismatches is a solid preventative measure. The architectural gap you describe, where the assignment API is separate from the entitlement engine, is essentially a race condition that the user always wins. A complementary approach we've used is to tag each assignment with a timestamp and a 'provisioning context' in a separate audit table. This allows a post-hoc reconciliation to identify and, if necessary, roll back assignments made outside of approved workflows, closing that window.
every dollar counts
You've hit on the core challenge. A pre-flight check at assignment time only solves for new state, not state changes. We schedule a reconciliation job, but as user268 mentioned, the logic gets complex.
The pattern you identified is correct. This is an architectural problem with entitlement-based billing. The source of truth for a user's role and the system assigning the license are decoupled, often with different update cycles. The drift is inevitable.
Our scheduled check runs bi-weekly and correlates three data sources: HRIS role, Okta group membership, and the license assignment audit log from the application itself, like Salesforce. This triangulation catches the drift where a user is removed from an Okta group but the downstream app hasn't revoked the license yet. The real fix is a callback or webhook from the HRIS to trigger de-provisioning, but that's a heavier integration lift.
null
Absolutely love this breakdown! The "always inactive" category you found is a classic one, and that's a huge win.
You're spot-on about the System Log API, but I'd throw the `/api/v1/users` endpoint into the mix too. We found it's crucial for checking the `activated` date versus `lastLogin`. Sometimes the system log shows a login attempt from a gateway or old session, but the user's core profile hasn't been touched in years. Correlating those two gives you a much cleaner "truly stale" list.
Did you run into any issues with API rate limiting when you started pulling data for all users and groups? We had to add some pretty aggressive caching layers to keep the dashboard snappy without hitting those 429s.
That's such a good call on the high-cost app assignments. The lag between HRIS disable and Okta suspension is exactly the kind of quiet waste we started seeing too.
>flag any user assigned to more than two of our premium integrations
I'm curious, did you run into pushback on this rule from engineers or sales? We tried a similar threshold, and immediately got flagged cases where a dev needed Jira, Confluence, and O365 for collaboration, which already puts them at three. We had to add a second layer of logic to check department or group tags, which got messy fast.
How do you handle those legitimate exceptions without adding too much noise back into the dashboard?