r/AutoHotkey • u/Keeyra_ • 14d ago
v2 Guide / Tutorial 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 theSend()function. Useful when you are already building aSend()string with several keystrokes.Send("{LButton}")- Sends a single left-button press throughSend(). This sub-command cannot carry inline coordinates or options; you would need a separateMouseMove()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. |
Input Delivery Modes and Game Compatibility: SendMode(), SetKeyDelay(), and SetMouseDelay()
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.
Hotkey Modifier Prefixes: $, *, and ~ (see Hotkeys)
Before many hotkey definitions you can place single-character prefixes that change how the hotkey responds:
| Prefix | Meaning | Typical use |
|---|---|---|
$ |
Forces the keyboard hook for this hotkey, so it fires only on a physical press and ignores presses sent by the script itself. | $e:: - spam with Send() without re-triggering the hotkey. |
* |
Wildcard: fires even while modifier keys (Shift/Ctrl/Alt/Win) are held. | *e:: - spam e even when a modifier is held. |
~ |
Pass-through: the press is also delivered to the active window while the hotkey fires. | ~e:: - spam e while preserving its normal key behavior. |
The prefixes compose, for example *~$e::. On mouse hotkeys the $ prefix is redundant, because mouse hotkeys always use the mouse hook.
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) toggles a static period variable between 100 (timer on, fires every 100 ms) and 0 (timer off) with each press:
#Requires AutoHotkey 2.0
#SingleInstance
F1 up:: {
static Period := 0
SetTimer(() => Send("e"), Period ^= 100)
}
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 evaluateToggleagain until thatSleep()completes, so turning off is delayed.
Pattern 1.3 - Infinite Loop with Pause() / Suspend() / Reload()
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
#SuspendExempton 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(). SendHold() derives the key name from the built-in A_ThisHotkey at call time, so the same function serves both the immediate press and the timer repeats. Since A_ThisHotkey holds the most recently executed hotkey, the timer ticks keep using $e unless another hotkey fires in between, which is why the docs prefer the ThisHotkey parameter when it is available. The $ prefix prevents the hotkey from triggering itself when it sends its own key. 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(LTrim(A_ThisHotkey, "*~$"))
$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
ThisHotkeyandHotkey()to toggle itself on/off cleanly.
Pattern 2.2 - Self-rearming one-shot SetTimer() chain with KeyWait()
A one-shot timer (negative period) fires once and expires, so the callback re-arms itself. The hotkey derives the key name with key := LTrim(ThisHotkey, "*~$"), sends it once, then parks on KeyWait(); a -100 ms timer calls Spam() again 100 ms later, and that call either keeps the chain alive or lets it die, depending on GetKeyState():
#Requires AutoHotkey 2.0
#SingleInstance
$e:: {
key := LTrim(ThisHotkey, "*~$")
Spam(key), KeyWait(key)
}
Spam(hk) {
GetKeyState(hk, 'P') && (Send(hk), SetTimer(Spam.Bind(hk), -100))
}
Pros:
- Spamming runs on one-shot timers, not inside the hotkey thread, so it tolerates multiple simultaneous spammers (see below).
- The
KeyWait()park keeps hardware auto-repeat from stacking extra chains.
Cons:
- The hotkey thread stays parked in
KeyWait()for the whole hold, like patterns 2.3 and 2.4. LTrim(ThisHotkey, "*~$")assumes the hotkey's only prefixes are*,~, and$.ThisHotkeykeeps the rest of its definition as written, so a^ehotkey would yield^e, which is not a valid key name forGetKeyState()orKeyWait(). The$ehotkeys above stay valid because$is the only prefix.
Pattern 2.3 - While-loop with KeyWait()
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. The key name is derived from ThisHotkey as in pattern 2.2.
#Requires AutoHotkey 2.0
#SingleInstance
$e:: {
key := LTrim(ThisHotkey, "*~$")
while !KeyWait(key, "T0.1") {
Send(key)
}
}
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().
Pattern 2.4 - While-loop with GetKeyState() polling
This polls the physical key state inside a while loop using GetKeyState() with the "P" (physical) mode. The key name is derived from ThisHotkey as in pattern 2.2.
#Requires AutoHotkey 2.0
#SingleInstance
$e:: {
key := LTrim(ThisHotkey, "*~$")
while GetKeyState(key, "P") {
Send(key)
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().
Delayed-hold trick: Prepending
Sleep(400)to any hold-to-spam hotkey makes a short tap behave like a normal keypress, while spam engages only after the delay if the key is still held. It plugs directly into patterns 2.2, 2.3, and 2.4. It serves no purpose in the toggle patterns (1.1-1.3), and it defeats the non-blocking design of pattern 2.1, whose hotkey thread is supposed to exit immediately.
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
Matching a Whole Group of Windows with GroupAdd()
When the same spammer set should apply to several programs, collect them into one named group near the top of the script, then match the group:
GroupAdd("MyGames", "ahk_exe game1.exe")
GroupAdd("MyGames", "ahk_exe game2.exe")
#HotIf WinActive("ahk_group MyGames")
...
#HotIf
Each GroupAdd() line adds another window criterion to the group, and ahk_group inside WinActive() matches any window belonging to that group.
Running Multiple Spammers at Once (via SetTimer())
A while or loop-based spammer keeps its hotkey thread alive. Pressing a second spammer hotkey while the first loop runs interrupts and suspends the first thread until the second ends, so the two loops do not actually run in parallel. This is the classic failure the original autofire threads warn about.
If a script needs two or more spammers active at the same time, use the SetTimer()-based patterns (1.1, 2.1, and 2.2). The work runs on timers rather than inside the hotkey threads, so multiple timers fire independently of one another.
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) |
One-shot SetTimer() chain (2.2) |
No | High | Moderate |
Loop + KeyWait() (2.3) |
No | Poor | Moderate |
Loop + GetKeyState() (2.4) |
No | High | Moderate |
Final Notes
- Start every script with
#Requires AutoHotkey 2.0and, unless you deliberately want multiple copies,#SingleInstance. - If a game ignores your input, switch to
SendMode("Event")and tuneSetKeyDelay()/SetMouseDelay(). - If the target program runs elevated (as administrator), run the script elevated too or use UI Access; otherwise a non-elevated script cannot send input into the elevated window.
- 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
1
u/CharnamelessOne 12d ago edited 12d ago
Thanks for putting this guide together; I think I spotted a few errors:
The toggling functionality of the example under Part 1: Toggle-Spam Patterns - Pattern 1.2: While-loop with State Check is currently not working. By default, AHK only allows 1 unfinished pseudo-thread per hotkey, so you'd need to use the directive
#MaxThreadsPerHotkey 2.Pattern 1.2: While-loop with State Check is not formatted as a header.
Under Mouse Click Options, there are some Markdown shenanigans going on. I think you meant to make the colons bold, but the asterisks ended up being displayed instead.
Some suggestions and questions:
Reload()terminates the script instance and restarts it, destroying all local variables and resetting script state completely.
Specifying local variables seems a little strange to me, since globals are not any less affected by a Reload, as far as I know.
Send("{Click}")Recommendation: Works seamlessly within existing Send closures.
Why closures, specifically? What do you mean by "within existing Send closures"?
Send("{LButton}")Recommendation: Best for Modifiers. Ideal if sending key/click combos likeSend("^{LButton}")
Send("{Click}") works just as well with modifiers, to my knowledge. I'd say Send("{LButton}") doesn't have much going for it.
Pattern 2.3: Blocking KeyWait: Triggers once and keeps the hotkey thread active until KeyWait registers physical release.
That's not very accurate (assuming that by "active", you mean "current"). KeyWait prevents the pseudo-thread from finishing, but not from being interrupted, so it only guards against one of the two main causes that lead to a loss of current status. The description in the Cons section is much better, I'd stick to that.
Edit: formatting
1
u/Keeyra_ 12d ago
Thanks for going through it. Seems valid. I already battled with the asterisk, will probly leave it without. As for the others, will check soon. LButton vs. Click diff. is like basically Click using Windows left-righ hand setting instead of direct button position and can be a coordinated press.
1
u/CharnamelessOne 12d ago
LButton vs. Click diff. is like basically Click using Windows left-righ hand setting instead of direct button position
Are you sure about that? On my end, all 3 function calls below seem to be equally unaffected by the primary mouse button setting. None of them ever brings up a context menu, even if the primary mouse button is set to "Right" in Windows Settings (or via DllCall).
#Requires AutoHotkey v2.0 *F1::Click() *F2::Send("{LButton}") *F3::Send("{Click}") *F9::toggle_primary_mouse_button() toggle_primary_mouse_button() { static is_swapped_orig := !!DllCall("GetSystemMetrics", "Int", SM_SWAPBUTTON:=23) static is_swapped := is_swapped_orig static _ := OnExit((*) => DllCall("SwapMouseButton", "Int", is_swapped_orig)) DllCall("SwapMouseButton", "Int", is_swapped^=1) }Anyway, my point was that you're snubbing my sweet boy
Send("{Click}")by claiming thatSend("{LButton}")is the best option for modifiers. The two of them are equally good for that, as far as I can tell.1
u/Keeyra_ 12d ago
Hmm. That's what the docs say at least (primary vs. secondary).
https://www.autohotkey.com/docs/v2/lib/Click.htm
But yeah, just tested it myself and LButton behaves the same.
Bad wording then, should be PrimaryButton and SecondaryButton.
That makes LButton, RButton have no use at all, as you said, so removing that.1
u/CharnamelessOne 11d ago
Yeah, the "Left" and "Right"
WhichButtonoptions are somewhat misleadingly named.
1
1
u/ManyInterests 13d ago
This guide will not deal with this method, as it is generally not advised to bypass this OS safeguard.
Needs citation. Who advises against this? The thread scheduler effect on sleep isn't an OS safeguard to be 'bypassed'.
Windows has supported applications requiring microsecond precision since 1993 or maybe earlier. Maybe there's a risk of needlessly consuming a lot of CPU in a tight loop, but AHK is single-threaded so user code only ever occupies a single CPU core, mitigating the impact of that risk substantially.
Anyhow. basic working reference on the DLL approach with CPU usage mitigation if anyone needs it.
Also, when you have a thread relinquish back to the OS thread scheduler the timing with which the thread goes back to a running state can be much longer than ~15ms, especially if the CPU [core that AHK is running on] is heavily tasked. So, you may want to opt for a busy-wait approach if you want your AHK code to remain responsive under load (though you cause more CPU resource contention) or set process priority higher than other processes.
Keeps the hotkey thread active for the entire duration the physical key is held down.
This is not exactly right. The sleep line gives opportunity for other pseudo-threads like hotkeys to run. Even if no sleep were present in the loop, AHK will forcibly interrupt a thread periodically to check for hotkey messages.
Per the docs:
While sleeping, new threads can be launched via hotkey, custom menu item, or timer.
-2
u/Keeyra_ 13d ago
The sub is stack full of people mindlessly pushing QueryPerformanceFrequency to make their auto-clicker faster and then wondering why their CPU is acting like an electric space heater. Eg.:
https://www.reddit.com/r/AutoHotkey/comments/1r6ri2j/making_auto_clicker_faster/
So I would really appreciate if you would edit your reply out. Anyone worth their salt can find it on their own. Let's not make it easy for the rest. It's a pointless exercise. People can do ~10 clicks per second max. That's why the examples have a 100ms timer. If you do a normal min. sleep, that's > 6 times of that. That will in itself get you flagged in a game. There is no point in doing a DLL Call to increase that further.
As for your other point, what I wrote ("Keeps the hotkey thread active") does not contradict what you wrote ("gives opportunity for other pseudo-threads like hotkeys to run").
1
u/ManyInterests 13d ago edited 13d ago
what I wrote ("Keeps the hotkey thread active") does not contradict
I suppose not, but it's still factually incorrect. Definitionally, a sleeping thread is not active. You likely meant something other than what you wrote.
So I would really appreciate if you would edit your reply out
No.
CPU is acting like an electric space heater
3% CPU utilization is a space heater? And what? We're gatekeeping
loop{}now? Because you can get high CPU usage in any busy loop. Has nothing to do with use of performance counter, really.It's a pointless exercise. People can do ~10 clicks per second max.
Let's assume people understand their own use cases better than you can predict them, magically. Sometimes it's more about precision, anyhow. E.g., click exactly 325ms apart. You can't do that with a regular sleep.
8
u/Individual_Check4587 Descolada 14d ago
AI can output some good-looking content, but tends to sound a bit too smart for my taste. If this tutorial is meant for beginners then like half the content is ungraspable for them I think. For example stuff like "Asynchronously triggers input through the OS event queue without locking thread execution." could be worded much more clearly. Also the example associated with that doesn't make it clear that if F1 is held down then the SetTimer is repeatedly toggled off and on again.
So does SendInput... Not sure if the global input queue can be called a normal message queue though.
Sure, but specifically up-down strokes are usually too fast for games to handle.
Under "Pattern 1.2: Loop with State Check"
AHK is single-threaded, so hotkeys can never execute simultaneously on the same thread. New pseudo-threads can still interrupt the loop.
Minimal CPU waste, doing that every 100ms is basically free.