I have done this before and can confirm that Session_End will only be called if you manually destroy the session (Session.Abandon). When you think about it, it makes sense. If the user is not on the website, the code will never be executed.
What I did was to store the hash table in the application state, containing the username and date-time the last time the user was seen on the site. For each page load, I would call a function that either inserted or updated this value for the current user. Then it will select the entire list and delete all records that are older than the session timeout (20 minutes or something else). Remember to use lock or sync to avoid race conditions when making changes to this list.
This has the added benefit of not only knowing how many people, but specifically which users.
If you do not have something unique, such as Username, you can use Session.SessionID. It must be unique to your site visitor.
But be careful, using the state variable of an instance of an application or application has its share of problems, since it will not be shared between processes in "Web Garden mode" or in configuration with multiple servers. For larger settings, you will need a more persistent medium, such as a database or distributed cache.
source share