Pick a test. Move, click, scroll.
Each test takes under a minute. Your results stay on this page and disappear when you close it.
Your mouse's polling rate is how many times per second it tells your computer where it is. Move your mouse in circles inside the box and keep going for about 5 seconds.
Each orange dot is one report from your mouse. The closer together the dots, the faster your mouse.
Click the box slowly, about once a second, 20 times, with the button you want to check. If one press shows up as 2 clicks, the switch inside is wearing out. That's the most common reason mice get replaced.
How fast can you click? The timer starts on your first click. This one measures your finger more than your mouse, so have fun with it.
Your mouse report
All four tests, in one place.
Same hand. Different mouse.
Polling rate is the number of position updates per second. More updates means the cursor sits closer to where your hand actually is.
One dot per update, 10x slower
The same movement at 4 polling rates. Watch how the gaps close up.
Nothing leaves this page
No account. No cookies. No analytics. No ads. The test runs on your computer, and your results vanish when you close the tab.
Got a Mac? Every polling test says 120.
macOS gives apps 1 mouse update per screen refresh, so browsers never see your real rate. We built a small Terminal tool that reads the mouse directly.
Got a Mac?
On a Mac, every mouse test says 120. That's your screen, not your mouse. This tiny tool shows the real number.
- Copy the tool.
- Paste it in Terminal. Press Cmd Space, type Terminal, then Cmd V and Return.
- Move your mouse. You'll see your number. Press Control C to stop.
Something not working?
If your Mac asks about Input Monitoring, allow Terminal, then type swift ~/hidrate.swift and press Return.
If it says swift isn't found, run xcode-select --install once and try again.
Done? You can delete hidrate.swift from your home folder. It doesn't save or send anything.
~ % swift ~/hidrate.swift Watching: Keychron Link (USB) Watching: MX Master 3 (Bluetooth Low Energy) Keychron Link (USB): 974 reports/s MX Master 3 (Bluetooth Low Energy): 129 reports/s
Same mouse, same Mac.
Read the code
// hidrate.swift (version 3)
// Counts the raw HID reports per second that each mouse sends to this Mac.
// This is what the mouse actually does, below the browser and below macOS event coalescing.
// Version 3 shows one line per physical mouse. Some receivers appear to macOS twice; the duplicate is skipped.
//
// Run it: swift hidrate.swift
// Stop it: Control+C
// The first run asks for Input Monitoring permission for Terminal
// (System Settings > Privacy & Security > Input Monitoring). Allow it, then run again.
import Foundation
import IOKit.hid
var counts: [String: Int] = [:] // key: device label
var seen = Set<String>()
var labels: [UnsafeMutableRawPointer: String] = [:]
let reportCallback: IOHIDReportCallback = { _, _, sender, _, _, _, length in
guard let sender = sender, length > 0 else { return }
let label = labels[sender] ?? "unknown device"
counts[label, default: 0] += 1
}
let deviceMatched: IOHIDDeviceCallback = { _, _, _, device in
let key = Unmanaged.passUnretained(device).toOpaque()
if labels[key] != nil { return }
let product = (IOHIDDeviceGetProperty(device, kIOHIDProductKey as CFString) as? String) ?? "mouse"
let transport = (IOHIDDeviceGetProperty(device, kIOHIDTransportKey as CFString) as? String) ?? "?"
let page = (IOHIDDeviceGetProperty(device, kIOHIDPrimaryUsagePageKey as CFString) as? Int) ?? 0
let usage = (IOHIDDeviceGetProperty(device, kIOHIDPrimaryUsageKey as CFString) as? Int) ?? 0
let location = (IOHIDDeviceGetProperty(device, kIOHIDLocationIDKey as CFString) as? Int) ?? 0
let dedupe = "\(product)|\(transport)|\(location)|\(page)|\(usage)"
if seen.contains(dedupe) { return }
seen.insert(dedupe)
let label = "\(product.trimmingCharacters(in: .whitespaces)) (\(transport))"
labels[key] = label
let reported = (IOHIDDeviceGetProperty(device, kIOHIDMaxInputReportSizeKey as CFString) as? Int) ?? 64
let size = max(reported, 8)
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: size)
IOHIDDeviceRegisterInputReportCallback(device, buffer, size, reportCallback, nil)
IOHIDDeviceScheduleWithRunLoop(device, CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue)
print("Watching: \(label)")
}
let manager = IOHIDManagerCreate(kCFAllocatorDefault, IOOptionBits(kIOHIDOptionsTypeNone))
let matching: [[String: Any]] = [
[kIOHIDDeviceUsagePageKey: kHIDPage_GenericDesktop, kIOHIDDeviceUsageKey: kHIDUsage_GD_Mouse],
[kIOHIDDeviceUsagePageKey: kHIDPage_GenericDesktop, kIOHIDDeviceUsageKey: kHIDUsage_GD_Pointer],
]
IOHIDManagerSetDeviceMatchingMultiple(manager, matching as CFArray)
IOHIDManagerRegisterDeviceMatchingCallback(manager, deviceMatched, nil)
IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue)
let result = IOHIDManagerOpen(manager, IOOptionBits(kIOHIDOptionsTypeNone))
if result != kIOReturnSuccess {
print("Could not open the HID manager (code \(result)).")
print("Give Terminal permission under System Settings > Privacy & Security > Input Monitoring, then run this again.")
exit(1)
}
print("Move the mouse in circles, steadily, without stopping. One line per second. Control+C to stop.")
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if counts.isEmpty { print("(no movement)"); return }
let line = counts.map { "\($0.key): \($0.value) reports/s" }.sorted().joined(separator: " ")
counts.removeAll()
print(line)
}
RunLoop.main.run()