If you're building a clipboard manager for macOS, the first question you'll hit is: how do I know when the user copies something? The answer is disappointingly simple — you poll. There's no delegate, no notification, no Combine publisher. You check a number on a timer and compare it to the last number you saw.
This surprises most developers. In 2026, with all of Apple's reactive frameworks, the clipboard is still a pull-only API. Here's why, and how to do it well.
Why there's no notification API
The macOS pasteboard is managed by pboard, a system daemon that acts as a simple data broker. Apps write data to it. Apps read data from it. The daemon doesn't maintain a subscriber list or push events.
This design is intentional. Consider what a notification system would require:
- Every app observing the clipboard would need to be woken on every copy
- Background apps — potentially dozens — would all activate simultaneously
- The system would need to manage observer lifecycles, deregistration, and ordering
For a feature that fires maybe once every few seconds at most, the overhead of a push system outweighs the simplicity of polling. Apple chose the simpler path, and frankly, it works.
The absence of a clipboard notification API isn't an oversight — it's a deliberate design choice that prioritizes system simplicity over developer convenience.
How changeCount works
NSPasteboard.general.changeCount is a monotonically increasing integer. Every time the pasteboard contents change — whether from a copy, a cut, a clearContents() call, or a programmatic write — the count increments.
The count is global across all apps. It doesn't reset when apps quit. It persists across the current login session (it resets on reboot).
changeCount edge cases
The count increments on clearContents() even if nothing is subsequently written. This means a change in count doesn't guarantee there's readable data. Always check for content after detecting a change. Also: the count can jump by more than 1 if an app calls clearContents() followed by writeObjects() — that's two increments.
The basic monitoring pattern is:
var lastCount = NSPasteboard.general.changeCount
// On each timer tick:
let current = NSPasteboard.general.changeCount
if current != lastCount {
lastCount = current
// Clipboard changed — read and process
}
Checking changeCount is a lightweight Mach IPC call to the pboard daemon. No data is transferred — just the integer. The actual clipboard data is only read when you explicitly request it.
Polling interval tradeoffs
The polling interval determines how quickly your app detects new clipboard content. Here's the tradeoff space:
The scenario that breaks slow polling: the user copies text, switches to another app, pastes it, switches back, and copies something new — all within a second. If your polling interval is longer than the gap between the two copies, you miss the first one entirely. It never appears in history.
At 0.5 seconds, this is nearly impossible to trigger in normal use. At 2 seconds, it happens regularly.
CPU impact in practice
Developers worry about the CPU cost of polling, but the numbers are reassuring. Checking changeCount involves:
- A Mach message send to the
pboarddaemon - The daemon reads an integer from memory
- A Mach message reply with the integer
Total time: under 10 microseconds. At 0.5-second intervals, that's 20 microseconds of CPU per second — approximately 0.002% of a single core. Activity Monitor won't even register it.
The timer itself has more overhead than the check. Use Timer.scheduledTimer with a tolerance value to let the system coalesce timer fires with other work:
let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
checkClipboard()
}
timer.tolerance = 0.1 // Allow ±100ms to save energy
Setting a tolerance on your polling timer lets macOS coalesce it with other system timers. This is especially important on laptops where timer coalescing directly reduces CPU wakeups and improves battery life. A 100ms tolerance on a 500ms timer is a good default.
Efficient polling patterns
Beyond the basic pattern, a few optimizations make polling more robust:
Skip your own writes. When your clipboard manager writes an item to the pasteboard (for pasting), the change count increments. Without a guard, you'd record your own paste as a new clipboard entry. Track when you're writing and ignore the next change.
Debounce rapid changes. Some apps (especially Electron-based ones) clear and rewrite the pasteboard multiple times during a single copy operation. If you detect a change, wait 50ms and check again before reading — you'll get the final, complete data instead of an intermediate state.
Respect App Nap. If your app is App Napped (no visible windows, no audio), macOS may throttle your timers significantly. For a menu bar app, this usually isn't an issue since the status item counts as a visible element. But test it.
Handle empty pasteboards gracefully. After detecting a change, the pasteboard might have no types you can read (e.g., proprietary app-specific data). Don't crash, don't log an error — just skip it.
Clipboard polling is one of those techniques that feels wrong to developers raised on reactive programming. But it's the right tool for this job: simple, efficient, and reliable. Every clipboard manager on macOS uses it, including QuietClip.
Efficient clipboard monitoring, built in.
QuietClip polls your clipboard with negligible CPU impact and stores everything locally. No cloud, no subscriptions. Free to start, $8.99 once for Pro.