Next up

Is your mouse
any good?

Four free tests for the mouse on your desk. Polling rate, buttons, double-clicks and click speed. Everything runs right here in your browser.

100% free No ads No tracking No sign-up No judgment
Got a Mac? Every polling test on a Mac says 120, ours too. That's your screen, not your mouse. See your real number.
Move your mouse. Look behind it.

See those dots? That's your mouse talking.

Every dot is one report your mouse sends to your computer, saying where it is. More dots per second means the cursor stays closer to your hand.

0
Move your mouse

Pick a test. Move, click, scroll.

Each test takes under a minute. Your results stay on this page and disappear when you close it.

Runs in your browser

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.

0Hz
Waiting for your mouse
Move your mouse in here, in circles

Each orange dot is one report from your mouse. The closer together the dots, the faster your mouse.

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.

125Hzoffice, Bluetooth
500Hzolder gaming
1000Hzgaming standard
4000Hzhigh end
125Hz = an update every 8 ms. 1000Hz = every 1 ms.
On Windows, use Chrome or Edge. The browser sees every update, so the number is your mouse's real rate.
On a Mac, every browser stops at your screen's refresh rate. The Mac tool shows the real number.
Bluetooth almost always runs slower than the receiver that came with your mouse. Test both if you have them.
A browser sees the updates that reach the page, so this is close, not lab exact. Run it 2 or 3 times.

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.

FreeNo adsNo trackingNo sign-up

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.

See your real number
Testing on a Mac?

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.

  1. Copy the tool.
  2. Paste it in Terminal. Press Cmd Space, type Terminal, then Cmd V and Return.
  3. 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.

Terminal
~ % 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
Browser said120
vs
Mac tool said974

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()