Windows Service or System Tray App? A Practical Comparison
Recently, I was assigned to monitor a third-party application to detect when a workstation is unlocked or a user logs in. Additionally, if the monitor goes off, the monitored app should be started.
Since the third-party app allows multiple instances running simultaneously and cannot be disabled, ensuring that only one instance remains active is crucial.
If the application is not running in the foreground, it should be brought to the front. If it is minimized, it should be maximized.
One challenge is that when the user closes the application, it goes to the SystemTray, which introduces several challenges.
So, let’s break it down.
The first step is to check whether the third-party app is running.
My app should monitor the third-party app as below:
- App not running → Launch it.
- App running → Further checks:
- Running with UI → Restore to foreground.
- Running without UI:
- In taskbar → Restore to foreground with full UI.
- Not in the taskbar → Assume running in SystemTray → Launch it.
Why Not Add This Logic to the App Itself?
We couldn’t modify the app directly, as the monitored app is an old legacy app, and we don’t have access to its source code.

Below is the journey for building a tray application rather than using a Windows Service to capture power events and session changes on Windows systems.
Why We Built a Tray App (Not a Service)
We initially considered using a Windows service to detect monitor on/off events, session lock/unlock, and potentially launch userapplications. However, we quickly encountered limitations due to modern Windows power management (especially S0 Low Power Idle) and security constraints around services.
Ultimately, we found that a tray application (running as a user-mode program) was more flexible, especially for monitor off/on detection and app-launch features. Below, we detail our findings and explain why the tray approach was the better choice.
Capturing Monitor Off/On Events
A Windows Service cannot easily capture monitor on/off transitions because these come via WM_POWERBROADCAST → PBT_POWERSETTINGCHANGE messages. To receive them, an application typically needs a window handle within the interactive user session.
By default, services run in Session 0 and do not have a visible window or message loop, meaning monitor on/off notifications never arrive. The only official way to receive them is through RegisterPowerSettingNotification, which ties notifications to a window—hence the need for a user-mode application (like a system tray application).
S0 Low Power Idle (Modern Standby)
Modern Windows laptops and tablets often use S0 Low Power Idle (also known as Modern Standby) instead of the classic S3 sleep state. Under S0, the system never truly “suspends” in the legacy sense, so a service’s OnPowerEvent will not see Suspend or Resume in the usual way.
A tray app can still catch “lightweight” power events—such as the monitor turning off—because it is attached to the user session and uses RegisterPowerSettingNotification. This works regardless of whether the system is in S0 or S3.
Session Lock/Unlock
A Windows Service can handle lock/unlock by overriding OnSessionChange with CanHandleSessionChangeEvent = true. In our tests, this worked well for logging lock/unlock events. However, once we also needed monitor on/off detection and the ability to launch user-facing applications, staying within a user-mode context became the simpler choice.
A tray app:
- Runs in the same user session.
- Has no issues launching interactive user programs (e.g., a custom third-party app).
- Can display icons, context menus, and notifications if needed.
Launching User Applications
One key requirement was the ability to launch another application upon certain power or session events. A desktop/tray app has no restrictions on creating new processes in the current user session.
By contrast, a Windows Service may face security constraints or require the (now deprecated) “Interact with desktop” property, which is largely disabled in modern Windows due to session isolation.
Testing the Windows Service Approach
- Lock/Unlock Detection. We created a service, set CanHandleSessionChangeEvent = true, and implemented OnSessionChange. This let us see lock/unlock or logon/logoff events, which worked as expected.
- Suspend/Resume Detection. We relied on OnPowerEvent to detect suspend/resume calls. However, on modern hardware with S0 Low Power Idle, these events never arrived, because the OS doesn’t follow the traditional S3 “sleep.” Therefore, we saw no suspend/resume logs.
- Monitor Off/On. We discovered that services do not receive monitor-off broadcasts by default. Windows does not treat display-off as a system suspend, so OnPowerEvent call is never triggered. The only official approach is RegisterPowerSettingNotification, which requires a window handle, meaning we needed a user-mode appin the interactive session.
Conclusion
The service approach worked fine for lock/unlock events but failed for monitor off/on and S0 power transitions.
We concluded that a user-mode (tray) application is the best solution for modern Windows systems if you need:
- Monitor state changes (display on/off).
- Session lock/unlock.
- Launch user-facing programs in response to these events.
A tray app runs in the user session, can display icons and handle messages, and easily calls RegisterPowerSettingNotification for power events.
Overall, it’s simpler, more reliable, and fully functional under S0 Low Power Idle—where a Windows Service fails to capture critical events.
Have questions or ideas? Let us know! This project taught us a lot about modern Windows power management, session isolation, and how to seamlessly launch user apps. If you’re building something similar, consider packaging it with Advanced Installer.