Description
WP-UserOnline shows how many people are on your site right now, and where they are. Members are named, guests and search bots are counted, and the whole thing refreshes itself in the background without a page reload.
Features
- A counter, a “who is browsing this page” line and a “who is browsing the site” line, each usable on its own.
- A full users online page, listing every visitor with the page they are on, where they came from and when they arrived.
- A sidebar widget offering any of those five combinations.
- Every string is a template you can edit, so the wording is yours rather than the plugin’s.
- Around three hundred search bots recognised by user agent, and a filter for adding your own.
- One admin screen with three tabs — who is online now, the settings, the templates — plus a line in the Dashboard’s At a Glance panel.
- A section on the WP-Stats page when that plugin is installed.
Donations
I spent most of my free time creating, updating, maintaining and supporting these plugins, if you really love my plugins and could spare me a couple of bucks, I will really appreciate it. If not feel free to use it without any obligations.
Usage
The simplest way is the widget. Go to WP-Admin -> Appearance -> Widgets, add the UserOnline widget to a sidebar, and pick which of the five statistics types it should show.
A classic theme can call the template tags directly instead, anywhere in sidebar.php, header.php or a template part:
<?php if ( function_exists( 'users_online' ) ) : ?>
<p>Users online: <span id="useronline-count"><?php users_online(); ?></span></p>
<?php endif; ?>
The element ids matter: useronline-count, useronline-browsing-site, useronline-browsing-page and useronline-details are what the refresh script looks for. A figure printed outside one of them is correct when the page loads and then stays where it is.
To give visitors a page of their own listing everyone online, create a page and put the shortcode in it:
[page_useronline]
Everything the plugin has is at WP-Admin -> WP-UserOnline, on three tabs: Users Online for who is here right now, Settings, and Templates for the wording of everything the plugin prints.
Showing Users Online In A Block
One block is available in the editor, under Widgets:
- Users Online — everyone on the site right now, grouped into members, guests and bots, with the page each of them is reading and the most-ever-online record underneath. The same listing
[page_useronline]produces.
It renders on the server, so the preview in the editor is the real listing rather than an approximation, and the page updates itself while a visitor is reading it exactly as the shortcode’s does. Only one goes in a post: the listing carries its own useronline-details element and the refresh script finds it by that name.
Rendering it records nobody. Previewing the block in the editor does not add you to the figures you are previewing, however many times the preview refreshes — visitors are recorded when a page is loaded, not when the listing is drawn.
The shortcode still works and is not going anywhere. [page_useronline] behaves exactly as it always has, and a page already containing it needs no change. The block calls the same code the shortcode calls, so the two render identically — use whichever suits the page.
WP-CLI
wp useronline list
wp useronline list --format=count
wp useronline count
The command reads and never writes. The table maintains itself — every visit purges rows that have timed out — and the admin screen offers no destructive action, so neither does the command.
REST API
GET /wp-json/useronline/v1/count
POST /wp-json/useronline/v1/visit
count reports how many are online now, the record and its date, and records nobody — so a monitoring script polling it does not appear in the figure it is reading. `visit` is the heartbeat: it records the caller and answers with one of the four views, given a `mode` of `count`, `browsing-site`, `browsing-page` or `details`, plus the `page_url` they are on.
Neither route takes a nonce, and that is deliberate. A nonce cannot authenticate a logged-out visitor: anonymous nonces come from one session every such caller shares, so requiring one would prove nothing while breaking every visitor who is not signed in. A page_url that does not resolve to this site is ignored, and a heartbeat writes nothing but the caller’s own row.
These routes are an addition. The admin-ajax.php wp_useronline action is unchanged and still supported.
Screenshots




Blocks
This plugin provides 1 block.
- Users Online Everyone on the site right now, with the page each of them is reading.
Installation
- Install and activate the plugin. Counting starts immediately.
- Show it where you want it: add the UserOnline widget to a sidebar, add the block, put
[page_useronline]on a page, or call the template tags from your theme. - Go to
WP-Admin -> WP-UserOnlinefor the timeout, the naming and the rest of the options.
FAQ
-
Creating A UserOnline Page
-
- Go to
WP-Admin -> Pages -> Add New - Type any title you like in the post’s title area
- If you ARE using nice permalinks, after typing the title, WordPress will generate the permalink to the page. You will see an ‘Edit’ link just beside the permalink.
- Click ‘Edit’ and type in
useronlinein the text field and click ‘Save’. - Type
[page_useronline]in the post’s content area - Click ‘Publish’
If you ARE NOT using nice permalinks, you need to go to
WP-Admin -> WP-UserOnline -> Settings(the Settings tab) and under ‘UserOnline URL’, you need to fill in the URL to the UserOnline Page you created above. - Go to
-
To Display Most Number Of Users Online
-
- Use:
<?php if ( function_exists( 'get_most_users_online' ) ) : ?> <p>Most Users Ever Online Is <?php echo get_most_users_online(); ?> On <?php echo get_most_users_online_date(); ?></p> <?php endif; ?> -
To Display Users Browsing Site
-
- Use:
<?php if ( function_exists( 'get_users_browsing_site' ) ) : ?> <div id="useronline-browsing-site"><?php echo get_users_browsing_site(); ?></div> <?php endif; ?> -
To Display Users Browsing A Page
-
- Use:
<?php if ( function_exists( 'get_users_browsing_page' ) ) : ?> <div id="useronline-browsing-page"><?php echo get_users_browsing_page(); ?></div> <?php endif; ?> -
Every visitor shows the same IP address
-
Your site is behind a reverse proxy or CDN — Cloudflare, a load balancer, nginx in
front of Apache — so the address PHP sees is the proxy’s, not the visitor’s.The real address is in the
X-Forwarded-Forheader, but WP-UserOnline ignores that
header by default: any client can send it with any value, so trusting it blindly lets
a visitor forge their address. Opt in only if a proxy you control actually sets it, by
adding this towp-config.phpabove the/* That's all, stop editing! */line:define( 'WP_USERONLINE_TRUST_PROXY', true );If you need to decide at runtime — say, only trust it for requests arriving from your
load balancer — use the filter instead:add_filter( 'wp_useronline_trust_proxy', function () { return isset( $_SERVER['REMOTE_ADDR'] ) && '10.0.0.1' === $_SERVER['REMOTE_ADDR']; } );With neither set, the plugin records
REMOTE_ADDR— correct on a plain host, and the
proxy’s address behind one.Both the constant and the filter were renamed in 4.0.0, from
USERONLINE_TRUST_PROXY
anduseronline_trust_proxy. The old names do nothing at all now, so if your site was
already opted in, rename them or every visitor will start showing the proxy’s address. -
Who can see the users online screen?
-
Anyone with
manage_options, which in practice means administrators. Before 4.0.0 the
screen was open to anyone who couldlist_users, which on many sites included editors.
To put it back that way, filter the capability — the context tells the Users Online tab
from the other two, and each tab checks its own, so this does not open the settings at
the same time:add_filter( 'wp_useronline_capability', function ( $capability, $context ) { return 'useronline' === $context ? 'list_users' : $capability; }, 10, 2 ); -
Can I change the list of search bots?
-
Yes.
wp_useronline_botsfilters the whole list, whose keys are the names shown in the
users online list and whose values are the case-insensitive fragment looked for in the
user agent:add_filter( 'wp_useronline_bots', function ( $bots ) { $bots['My Crawler'] = 'mycrawler'; return $bots; } ); -
The plugin will not activate
-
WP-UserOnline 4.0 and later requires WordPress 6.8 and PHP 8.2. WordPress checks both and
refuses to activate the plugin on anything older, telling you which one is short.To see what your host is running, look at
Tools -> Site Health -> Info -> Server, or
install WP-ServerInfo.If you cannot upgrade, WP-UserOnline 3.0.0 is the last release supporting WordPress 6.0
and PHP 7.4.
Reviews
Contributors & Developers
“WP-UserOnline” is open source software. The following people have contributed to this plugin.
Contributors“WP-UserOnline” has been translated into 20 locales. Thank you to the translators for their contributions.
Translate “WP-UserOnline” into your language.
Interested in development?
Browse the code, check out the SVN repository, or subscribe to the development log by RSS.
Changelog
4.0.0
- FIXED: A recorded location could be a protocol-relative URL, which is an absolute one wearing a path’s clothes.
https://example.com//evil.com/parses with this site’s host, passed the host check, was stored as//evil.com/, and came out ofesc_url()untouched — so the public listing carried a link to somebody else’s site. Leading slashes now collapse to one, on the submitted URL and onREQUEST_URIalike - FIXED: The listing withheld the location of a visitor who was in the dashboard, but published their referrer from inside the same branch without ever looking at it. Anyone who clicked a front-end link from an admin screen therefore published the whole admin URL — screen, post ID and
_wpnonce— to anonymous readers, because a same-origin navigation carries the full URL under WordPress’s referrer policy. The referrer is now tested in its own right, and both tests compare against the site’s own admin path rather than the literal stringwp-admin, which never matched on a site that had moved it - FIXED: A single address could fill the table and permanently inflate “most users ever online” — a figure with no reset anywhere — by varying its user agent on each request, which defeated the de-duplication and left one row per second standing for the whole timeout. At most twenty rows per address are kept now;
wp_useronline_max_per_addressraises the ceiling, or zero removes it - FIXED: The refresh endpoint authenticates by cookie, so a cross-site form post ran it as whoever was signed in and replaced their row with a page and title of the attacker’s choosing, published under their name. A signed-in caller now needs a nonce; a logged-out one still does not, because an anonymous nonce is shared by every anonymous visitor and proves nothing
- NEW: A
wp useronlineWP-CLI command —listandcount. It reads and never writes. - NEW: A
useronline/v1REST API for reading the counts and for the visitor heartbeat. Theadmin-ajax.phpwp_useronlineaction is unchanged and still supported. - NEW: A Users Online block, under Widgets in the editor, showing the same listing as
[page_useronline]. The shortcode is unchanged and still supported; the block is an addition beside it, calls the same code, and records nobody when it renders. - BREAKING: Requires WordPress 6.8 and PHP 8.2, up from 6.0 and 7.4.
- BREAKING: Every filter the plugin fires is renamed and the old names are dropped, with no deprecation shims:
useronline_bots,useronline_buckets,useronline_custom_template,useronline_page,useronline_display_useranduseronline_trust_proxyall becomewp_useronline_*. This voids the promise made in the 3.0.0 changelog that the filters were unchanged, and is why this release is 4.0.0 rather than 3.0.1. - BREAKING:
USERONLINE_TRUST_PROXYis nowWP_USERONLINE_TRUST_PROXY. A site still defining the old name silently stops trusting its proxy and starts recording the proxy’s address for every visitor. See the FAQ. - BREAKING: The plugin has one screen, at
admin.php?page=wp-useronlineunder a top-level WP-UserOnline menu, with three tabs: Users Online, Settings and Templates. The report used to be under Dashboard and the settings under Settings. - BREAKING: Every tab now requires
manage_options. The users online listing previously needed onlylist_users. The newwp_useronline_capabilityfilter puts it back, for that tab alone. - BREAKING: The option rows are renamed.
useronlinebecomeswp_useronline_optionsanduseronline_mostbecomeswp_useronline_most, which is no longer autoloaded. Your settings are migrated automatically on the first load after the update. - BREAKING: The shared, unprefixed
stats_displayoption row is no longer read. WP-Stats integration is now a setting of this plugin’s own, and WP-Stats asks each plugin for its section through thewp_stats_sectionsfilter rather than reading anybody’s options. Update all seven WP-Stats-aware plugins together; see the Upgrade Notice. - BREAKING: Every class is renamed to
WP_UserOnline_*.UserOnline,UserOnline_Template,UserOnline_Optionsand the rest no longer exist under those names. - BREAKING: The refresh script is at
js/wp-useronline.jsrather thanuseronline.js, its localised object iswpUserOnlineL10nrather thanuseronlineL10n, and the admin-ajax action it posts iswp_useronlinerather thanuseronline. - NEW: The upgrade markers live in their own
wp_useronline_versionoption row, holding exactlypluginanddb, and are written together in one update at the end of the upgrade. - NEW: WP-Stats integration is a setting of the plugin’s own, on the Settings tab under WP-Stats, instead of a checkbox added to the WP-Stats options page.
- NEW:
wp_useronline_capabilityfilters the capability each tab requires, with a context saying which one is asking. The page itself is registered under the report’s capability and every tab checks its own, so widening the listing cannot hand over the settings form. - NEW: The templates are on a Templates tab of their own rather than below the settings. Both tabs post one setting into one option row, and the sanitiser merges what a tab submitted over what is stored, so saving one cannot blank the other.
- NEW: The Restore Defaults behaviour is a proper enqueued script,
js/wp-useronline-admin.js, rather than markup printed inside the page. - NEW: The JavaScript is covered by vitest and jsdom, and the PHP suite runs on the network as well as on a single site.
- CHANGED:
WP_UserOnline_Installowns the table, the migration and the markers, so install and uninstall sit beside each other and neither can drift. - FIXED: The upgrade markers no longer live inside the settings array. They were kept under a reserved
versionskey that the settings form never posted, so every save had to rescue them from the stored value by hand — which is the arrangement behind the 3.0.0 bug where the marker could not be saved at all once the settings screen had been loaded. They have a row of their own now, and the settings sanitiser has no business with them. - FIXED: The settings screen no longer emits an inline
styleattribute or an inline<script>block. - FIXED: The WP-Stats checkbox posts an explicit off. An unticked box posts nothing at all, and the sanitiser keeps whatever a tab did not submit, so without it the section could be switched on and never off again.
- FIXED: The periodic refresh no longer nests the
useronline-detailscontainer inside itself. The detailed report is answered as a whole container, and the refresh wrote that answer inside the container it was already in — so a page left open gained another copy of the same element id every timeout, growing a level deeper each time. Nothing errored and the figures kept updating, but the markup was invalid and any theme CSS or JavaScript keyed to#useronline-detailswas looking at a different element after the first refresh. The refresh replaces that container now instead of filling it. - NOTE: The template tags, the
[page_useronline]shortcode and theuseronline-count,useronline-browsing-site,useronline-browsing-pageanduseronline-detailselement ids are unchanged. Everything else that was public has moved; see the Upgrade Notice. - NOTE: The “on”, “url” and “referral” labels in the detailed listing carry translator context now, because a single word out of context is not enough to translate from. Those three msgids changed, so existing translations of them fall back to English until they are retranslated
