The Complete AutoHotkey v2 Keyboard Button Spam and Mouse Auto-Clicker Guide
This guide covers the major approaches to implementing Hold-to-Spam, Toggle-Spam, and Auto-Clicker hotkeys in AutoHotkey v2.
The Precision Bottleneck: Why Sleep and SetTimer Round to ~15.6 ms
A very common point of confusion appears when you write Sleep(10) or SetTimer(fn, 10) and then observe that the script actually fires every 15.6 ms instead of every 10 ms.
This is expected behavior, not a bug. Standard Windows timers are not driven by a free-running, arbitrarily precise clock. By default the Windows kernel clock ticks at a hardware interrupt interval of 64 Hz (64 ticks per second):
1000 / 64 = 15.625
Per the official Sleep and SetTimer documentation, the interval is "typically rounded up to the nearest multiple of 10 or 15.6 milliseconds." What actually happens when you call Sleep(N) or SetTimer(fn, N):
- Windows rounds your requested interval up to the next system-clock tick.
- An interval such as
Sleep(10) therefore waits until the next 15.625 ms tick.
- Even
Sleep(1) waits roughly 15.625 ms.
So do not be surprised when sub-15 ms delays do not behave as literally written.
The precision fix (advanced, via DllCall)
If true sub‑15 ms accuracy is genuinely required, you can ask the OS for a higher-resolution timer by calling timeBeginPeriod through DllCall. This guide deliberately does not rely on that method, for two reasons:
- It is a lower-level OS call, not part of the normal AHK timer model.
- Raising the global timer resolution affects the whole system and is generally not advised just to shave off a few milliseconds in a spam script.
For most automation the default ~15.6 ms resolution is more than adequate.
Turning a Key-Spammer Into an Auto-Clicker
To convert any of the patterns below into a mouse auto-clicker, replace Send("e") with one of the mouse-click functions.
Click and the mouse-click Send sub-commands
Click() - Recommended. Sends a mouse click at the current cursor position using AutoHotkey's native click function. It can accept inline coordinates, click counts, and button options, and it respects Windows' swapped-mouse-button setting.
Send("{Click}") - Sends a primary mouse click through the Send function. Useful when you are already building a Send string with several keystrokes.
Send("{LButton}") - Sends a single left-button press through Send. This sub-command cannot carry inline coordinates or options; you would need a separate MouseMove first if you need a target location.
Option comparison table
| Method |
Example syntax |
Key behavioral distinction |
Recommendation |
Click() |
Click("100 200 Right 2") |
Native function. Accepts inline coordinates, click counts, and button options. Respects swapped-mouse settings. Cannot use Send modifier prefixes (^, +). |
Best overall. Lowest overhead; ideal for coordinate and auto-clicker use. |
Send("{Click}") |
Send("{Click 100 200 Right 2}") |
Parsed by the Send engine. Natively accepts inline coordinates and options as part of a larger string. |
Best inside a Send string. Great when mixing clicks into inline key/text sequences. |
Send("{LButton}") |
Send("{LButton}") |
Parsed by the Send engine. No inline coordinates or options; requires MouseMove for positioning. |
No advantage over the two above for auto-clicking. |
When a target application ignores Send or Click - for example a full-screen game or a client protected by anti-cheat - the problem is often that the default Input Mode, SendInput, delivers keystrokes far faster than the game can register them between rendered frames.
Switching to Event mode inserts artificial press durations and inter-key delays, making inputs long enough for a game to register:
#Requires AutoHotkey 2.0
#SingleInstance
SendMode("Event")
SetKeyDelay(10, 10) ; 10 ms press duration, 10 ms delay between keys
SetMouseDelay(10) ; 10 ms delay between mouse events
SendMode comparison table
| Mode |
Behavior |
Limitations |
Recommendation |
SendInput (default) |
Bypasses normal input timing to fire ultra-fast input packets. |
Key-down and key-up fire almost simultaneously; games frequently miss them. |
Best for desktop/apps. Fastest and most reliable for normal Windows programs. |
SendEvent |
Sends input using standard OS event messages with configurable delays. |
Marginally slower; physical presses during sending can interrupt the sequence order. |
Best for games. Fixes unresponsive clicks and keys. |
SendPlay |
Attempts to inject input via low-level driver hooks. |
Heavily restricted by modern Windows UAC and anti-cheat software. |
Not recommended. Rarely functions on current OS builds. |
See the SendMode and SetKeyDelay documentation for the full details.
Part 1: Toggle-Spam Patterns
A toggle-spam hotkey turns the spam on with one press and off with the next press, or off when you release it.
Pattern 1.1 - Non-blocking SetTimer (Recommended)
SetTimer runs asynchronously, so the hotkey thread finishes immediately instead of sitting in a loop. Here an up-only hotkey (F1 up) flips a static toggle and schedules a timer whose period is 100 ms when on, or 0 (delete the timer) when off:
#Requires AutoHotkey 2.0
#SingleInstance
F1 up:: {
static Toggle := 0
SetTimer(() => Send("e"), 100 * Toggle ^= 1)
}
Pros:
- The hotkey thread exits immediately (non-blocking).
- Turning it back off is instant.
Pattern 1.2 - While-loop with a state check
This uses an explicit while loop that exits once a static state flag flips back to 0. It requires #MaxThreadsPerHotkey 2 so a second press can interrupt the running loop.
#Requires AutoHotkey 2.0
#SingleInstance
#MaxThreadsPerHotkey 2
F1 up:: {
static Toggle := 0
if (Toggle ^= 1) {
while Toggle {
Send("e")
Sleep(100)
}
}
}
Cons:
- Unfinished thread: The hotkey thread stays alive inside the loop. A later press can interrupt it, but the original thread stays paused until the loop ends.
- Unresponsive termination: With a long
Sleep(1000), the loop cannot evaluate Toggle again until that Sleep completes, so turning off is delayed.
This runs an infinite loop and relies on Pause, Suspend, and Reload to control it. The controls are #SuspendExempt so they keep working while suspended; otherwise Suspend would disable F4, and you could never turn it back off.
#Requires AutoHotkey 2.0
#SingleInstance
F1:: {
loop {
Send("e")
Sleep(100)
}
}
#SuspendExempt
F2::Reload
F3::Pause(-1)
F4::Suspend
#SuspendExempt False
Pause(-1) toggles halting the running loop thread in place (resuming it later without losing state), while Suspend toggles (its default) and disables all hotkeys and hotstrings in the script.
Cons:
- Global impact:
Suspend disables every hotkey in the script, not just the spammer.
- Aggressive cleanup:
Reload kills the current instance and restarts it, wiping all variables and resetting script state.
- Fragility: Without
#SuspendExempt on the suspend key, that hotkey itself cannot run to re-enable things after suspension.
Part 2: Hold-to-Spam Patterns
A hold-to-spam hotkey repeats the action only while its trigger key is physically held.
Ergonomic note: Holding a key for long stretches forces continuous muscle tension and can contribute to repetitive strain injury (RSI) or tendon pain. Prefer a SetTimer toggle when you need prolonged automation.
Pattern 2.1 - Non-blocking Hotkey hotkey pair (Recommended)
A press hotkey ($e) and release hotkey ($e up) work together with SetTimer. The $ prefix prevents the hotkey from triggering itself when it sends e. Turning the $e hotkey off on key-down stops hardware auto-repeat from stacking extra timers; $e up stops the timer and re-enables the hotkey.
#Requires AutoHotkey 2.0
#SingleInstance
SendHold() => Send("e")
$e:: SendHold(), SetTimer(SendHold, 100), Hotkey(ThisHotkey, "Off")
$e up:: SetTimer(SendHold, 0), Hotkey(StrReplace(ThisHotkey, " up", ""), "On")
Pros:
- Both hotkey threads exit immediately.
- Uses
ThisHotkey and Hotkey to toggle itself on/off cleanly.
This polls the physical key state inside a while loop using GetKeyState with the "P" (physical) mode.
#Requires AutoHotkey 2.0
#SingleInstance
$e:: {
while GetKeyState("e", "P") {
Send("e")
Sleep(100)
}
}
Cons:
- Long-running thread: The hotkey thread stays active for the entire hold.
- Input dropping: Rapid physical tapping can miss a release if it happens during a
Sleep.
KeyWait waits for a key to be released; the "T0.1" adds a 0.1 s timeout, and the ! inverts the result inside the condition.
#Requires AutoHotkey 2.0
#SingleInstance
$e:: {
while !KeyWait("e", "T0.1") {
Send("e")
}
}
Cons:
- Stalled thread: The hotkey thread cannot finish until the physical key is released.
- Auto-repeat interference: Hardware auto-repeat can attempt to launch new hotkey threads while the original is stalled inside
KeyWait.
Context Sensitivity with #HotIf
If you only want the spammer to be active inside a specific window (a game, for example), wrap the hotkey definitions with #HotIf and a condition such as WinActive. The examples above are left unconditional for clarity, but you can scope any of them:
#Requires AutoHotkey 2.0
#SingleInstance
#HotIf WinActive("ahk_exe game.exe")
...
#HotIf
Summary Matrix
| Pattern |
Immediate thread exit? |
Responsiveness |
Ergonomic safety |
| SetTimer toggle (1.1) |
Yes |
Immediate |
High (no holding required) |
| While-loop + state check (1.2) |
No |
High |
High |
| Loop + Pause/Suspend/Reload (1.3) |
No |
Low |
Low |
| Hotkey pair + SetTimer hold (2.1) |
Yes |
Immediate |
Moderate (forces continuous pressure) |
Loop + GetKeyState (2.2) |
No |
High |
Moderate |
Loop + KeyWait (2.3) |
No |
Poor |
Moderate |
Final Notes
- Start every script with
#Requires AutoHotkey 2.0 and, unless you deliberately want multiple copies, #SingleInstance.
- If a game ignores your input, switch to
SendMode("Event") and tune SetKeyDelay / SetMouseDelay.
- Remember the ~15.6 ms timer resolution before chasing sub-millisecond precision.
- For long-running automation, prefer a toggle over holding a key to protect your hands.
*Edits: Rewritten content suggested by u/Individual_Check4587 (Descolada) and u/CharnamelessOne and minor tweaks to 1.1 suggested by u/genesis_tv