A gaming mouse may be configured to 1000Hz, 4000Hz, or even 8000Hz, but open a browser-based test and you may not always see exactly the same number.
Why?
The obvious assumption is that if a mouse reports at 1000Hz, JavaScript should receive 1000 mouse events every second.
That sounds reasonable, but browsers do not work quite that directly.
JavaScript can measure mouse movement timing, but it does not directly read the USB polling rate from your mouse hardware. What it sees is the stream of pointer events that reaches the browser after passing through the operating system and browser event-processing layers.
Understanding that difference is important if you are building a mouse polling rate test, an input visualizer, a browser game, or any application that deals with high-frequency mouse input.
What Does Mouse Polling Rate Mean?
Mouse polling rate describes how frequently a mouse reports updated information to the computer.
For example:
Polling Rate Approximate Interval
125Hz 8ms
250Hz 4ms
500Hz 2ms
1000Hz 1ms
2000Hz 0.5ms
4000Hz 0.25ms
8000Hz 0.125ms
The relationship is simple:
pollingRate = 1000 / intervalInMilliseconds;
If two observable updates are exactly 1ms apart, that interval corresponds to roughly 1000Hz.
But the word observable matters.
A browser does not sit directly between the mouse sensor and the USB controller.
A simplified input path looks more like this:
Mouse hardware
↓
Mouse firmware
↓
USB or wireless connection
↓
Operating system
↓
Browser input processing
↓
Pointer event
↓
JavaScript
Your JavaScript code operates near the end of this chain.
That is why a browser measurement and a hardware-configured polling rate should not automatically be treated as the same measurement.
How to Measure Mouse Polling Rate in JavaScript
The simplest approach is listening for pointer movement.
let previousTime = null;
window.addEventListener("pointermove", (event) => {
if (previousTime !== null) {
const interval = event.timeStamp - previousTime;
const hz = 1000 / interval;
console.log(`${hz.toFixed(0)} Hz`);
}
previousTime = event.timeStamp;
});
The idea is straightforward.
Every time a pointermove event arrives, we calculate the time since the previous event and convert that interval into an estimated frequency.
For example:
2ms interval → approximately 500Hz
1ms interval → approximately 1000Hz
0.5ms interval → approximately 2000Hz
But this simple implementation has a major limitation.
It measures dispatched pointermove events.
It does not prove that every hardware report generated by the mouse produced an individual JavaScript event.
`
Why pointermove May Show Fewer Events
`
Browsers have to balance input processing with rendering, JavaScript execution, layout work, network activity, and everything else happening on the page.
Because of that, browsers are allowed to combine multiple pointer updates into fewer dispatched events.
This is called event coalescing.
Imagine the browser receives several position updates:
Update 1
Update 2
Update 3
Update 4
Instead of executing your JavaScript listener four separate times, the browser may combine some of those updates into one dispatched pointermove event.
This reduces event-handling overhead.
The W3C Pointer Events specification explicitly allows browsers to delay and coalesce pointermove events for performance reasons.
For normal interfaces, this behavior is usually beneficial.
For mouse-frequency measurements, however, it creates an important distinction:
JavaScript callback frequency is not necessarily identical to mouse hardware report frequency.
pointermove vs pointerrawupdate
Developers working with high-frequency pointer input should also know about:
pointerrawupdate
A simple listener looks like this:
window.addEventListener("pointerrawupdate", (event) => {
console.log(event.timeStamp);
});
According to MDN, browsers may delay pointermove events for performance, while pointerrawupdate is intended to be dispatched as soon and as frequently as the browser can produce those events.
That makes pointerrawupdate interesting for applications such as:
high-frequency pointer measurement
drawing applications
low-latency dragging
input visualizers
experimental browser input tools
But the name can be misleading.
pointerrawupdate Does Not Mean Raw USB Access
Using pointerrawupdate does not give JavaScript direct access to mouse firmware or USB packets.
The operating system and browser are still involved.
The Pointer Events specification also allows pointerrawupdate events themselves to be coalesced when JavaScript cannot process every update immediately.
So this would be an unsafe conclusion:
My JavaScript received 780 pointerrawupdate callbacks, therefore my mouse hardware is running at exactly 780Hz.
A better interpretation is:
The browser exposed this pointer-event pattern during the measurement.
That distinction makes the result technically much more defensible.
getCoalescedEvents() Can Reveal More Samples
Pointer Events provides another useful API:
event.getCoalescedEvents()
When several pointer updates are merged into one event, getCoalescedEvents() can expose the individual pointer events that were combined.
MDN describes it as a way to retrieve the events that were coalesced into a single pointermove or pointerrawupdate event.
A simple example:
window.addEventListener("pointermove", (event) => {
const coalesced = event.getCoalescedEvents?.() ?? [];
if (coalesced.length) {
for (const sample of coalesced) {
console.log(sample.timeStamp);
}
} else {
console.log(event.timeStamp);
}
});
Now we can distinguish between:
JavaScript callbacks
and:
Pointer samples available inside those callbacks
Those numbers may be different.
That is particularly important when investigating high-polling-rate mice.
A Better JavaScript Mouse Polling Rate Experiment
Instead of displaying the highest Hz value immediately, collect the timing data first.
Here is a more useful starting point:
const intervals = [];
let previousTime = null;
function processSample(sample) {
const currentTime = sample.timeStamp;
if (previousTime !== null) {
const interval = currentTime - previousTime;
if (interval > 0) {
intervals.push(interval);
}
}
previousTime = currentTime;
}
window.addEventListener("pointermove", (event) => {
const coalesced = event.getCoalescedEvents?.() ?? [];
if (coalesced.length) {
coalesced.forEach(processSample);
} else {
processSample(event);
}
});
Once enough samples have been collected, you can analyze the distribution instead of trusting one event.
For an individual sample:
const hz = 1000 / interval;
But one sample should not define the result.
Why Peak Polling Rate Can Be Misleading
Suppose your intervals look like this:
1.02ms
0.98ms
1.01ms
0.99ms
0.31ms
1.03ms
That 0.31ms interval converts to more than 3000Hz.
Does that mean your 1000Hz mouse suddenly produced a stable 3000Hz polling rate?
Not necessarily.
It could simply be an outlier caused by event scheduling, timing behavior, coalescing, or another part of the browser environment.
This is why showing only:
Peak: 3225Hz
can produce a very misleading mouse polling rate test.
A stronger measurement looks at several things together:
number of samples
average interval
median interval
consistency
minimum and maximum intervals
outliers
repeated test runs
The overall pattern is usually more informative than the largest number observed during one movement.
### Consistency Matters Too
Consider these two sets of intervals:
Test A
1.0
1.0
1.0
1.0
1.0
and:
Test B
0.4
1.6
0.6
1.4
1.0
Both can produce a similar average.
But their timing behavior is clearly different.
That is why I prefer thinking about polling-rate measurement as a distribution of observed event intervals, rather than one magical Hz number.
For developers building input tools, this also opens the door to showing useful metrics such as jitter or interval consistency instead of simply displaying a peak result.
Don't Let Your Test Affect the Measurement
There is another problem that is surprisingly easy to create yourself.
Imagine running this hundreds or thousands of times per second:
window.addEventListener("pointerrawupdate", (event) => {
result.textContent = event.timeStamp;
updateGraph();
calculateStatistics();
rebuildResults();
saveData();
});
The event handler is now performing DOM updates, calculations, graph rendering, and data processing during the measurement itself.
At sufficiently high event frequencies, the test can start interfering with the environment it is attempting to measure.
MDN specifically warns that high-frequency pointerrawupdate listeners can negatively affect page performance when the application cannot process the events quickly enough.
A cleaner architecture is:
const samples = [];
window.addEventListener("pointerrawupdate", (event) => {
samples.push(event.timeStamp);
});
Then update the interface separately:
function render() {
// Render current results here.
requestAnimationFrame(render);
}
requestAnimationFrame(render);
Now input collection and visual rendering are separated.
That makes the test easier to reason about and reduces unnecessary work inside the high-frequency event listener.
Browser Support Also Matters
You should not assume that every browser exposes the same Pointer Events features.
For pointerrawupdate, use feature detection:
const supportsRawPointer =
"onpointerrawupdate" in window;
For getCoalescedEvents():
const supportsCoalescedEvents =
typeof PointerEvent !== "undefined" &&
typeof PointerEvent.prototype.getCoalescedEvents === "function";
If an API is unavailable, your application should gracefully fall back rather than failing.
This is particularly important for a public mouse polling rate test because visitors may arrive using different browser engines, operating systems, and devices.
MDN currently marks both pointerrawupdate and getCoalescedEvents() as having limited availability rather than universal Baseline support.
Why 4000Hz and 8000Hz Mice Make This More Interesting
At 125Hz, the theoretical interval between reports is around 8ms.
At 1000Hz, it is around 1ms.
At 8000Hz:
1000 / 8000 = 0.125ms
That is an extremely short interval.
As the configured polling rate increases, factors outside the mouse itself become increasingly relevant to what JavaScript can observe.
These can include:
operating-system input handling
browser scheduling
event coalescing
CPU workload
JavaScript execution
timer precision
browser support
the implementation of the test itself
This is why seeing something below exactly 4000Hz or 8000Hz in a browser does not, by itself, prove that the mouse is defective or configured incorrectly.
The browser is an observation layer, not a hardware analyzer.
What Does a Browser Mouse Polling Rate Test Actually Measure?
This is the most useful conclusion from the experiment:
A browser-based mouse polling rate test measures the timing and frequency of pointer updates observable inside the browser.
It does not directly inspect mouse firmware.
It does not read USB reports directly.
And it should not claim that every JavaScript event corresponds one-to-one with a physical mouse report.
I use the same measurement boundary in my browser-based Mouse Polling Rate Test. The result is intended to represent browser-observed input timing, so users can test movement patterns and compare repeated runs without pretending JavaScript has direct access to the mouse's hardware reporting layer.
That is a much more useful way to interpret the result.
So, Can JavaScript Measure Mouse Polling Rate?
Yes, with an important limitation.
JavaScript can measure the timing of mouse or pointer events exposed by the browser and use those intervals to estimate an observed event frequency.
But it cannot directly verify every USB polling report generated by the hardware.
For most browser-based testing, the better question is therefore not:
“What is the exact hardware polling rate?”
It is:
“What mouse input frequency and timing can this browser observe under the current setup?”
Once that measurement boundary is clear, JavaScript becomes genuinely useful for comparing:
different polling-rate settings
browsers
wired vs wireless configurations
repeated test runs
input consistency
different systems
without overstating what the browser can actually see.
Try the Experiment Yourself
If you have a 1000Hz, 2000Hz, 4000Hz, or 8000Hz mouse, compare:
pointermove
with:
pointerrawupdate
and, where supported:
getCoalescedEvents()
I'm especially interested in one thing:
Do you see a meaningful difference between pointermove and pointerrawupdate as the configured polling rate increases?
If you test it, share your mouse polling-rate setting, browser, and what you observe. Comparing results across different systems could reveal some interesting browser behavior.
References
W3C: Pointer Events specification
MDN: pointerrawupdate event
MDN: getCoalescedEvents()
Top comments (0)