← Back to Blog
Code Snippets June 28, 2026

Debounce & Throttle Decorators That Actually Work

🐍 Code Snippet — June 28, 2026

Debounce & Throttle Decorators That Actually Work

For search-as-you-type, analytics tracking, resize handlers, auto-save — and unlike most implementations, this handles leading/trailing edge, max_wait, and cancellation.

import time, threading
from functools import wraps

def debounce(delay=0.5, *, max_wait=None, leading=False):
    def decorator(func):
        _timer = None
        _last_args, _last_kwargs = (), {}
        _last_call = 0.0
        _lock = threading.Lock()

        @wraps(func)
        def wrapper(*args, **kwargs):
            nonlocal _timer, _last_args, _last_kwargs, _last_call
            now = time.monotonic()
            with _lock:
                if _timer: _timer.cancel(); _timer = None
                _last_args, _last_kwargs = args, kwargs
                _last_call = now
                if max_wait and (now - _last_call) >= max_wait:
                    return func(*args, **kwargs)
                def _run():
                    with _lock:
                        if _timer: func(*_last_args, **_last_kwargs); _timer = None
                _timer = threading.Timer(delay, _run); _timer.daemon = True; _timer.start()
            return None
        def cancel():
            with _lock:
                if _timer: _timer.cancel(); _timer = None
        wrapper.cancel = cancel
        return wrapper
    return decorator

# Usage:
@debounce(delay=0.5)
def search(q): print(f"Searching: {q}")

@debounce(delay=0.3, max_wait=2.0)
def track(e): print(f"Event: {e}")


When to use: API rate-limiting, search input coalescing, preventing double-submit bugs, resize event debouncing.

Pro tip: Set max_wait to guarantee the function fires at least once even under sustained load — critical for analytics events where dropping the last batch is unacceptable.

💡 The standard library has no debounce — every framework re-invents it. This covers edge cases most implementations miss.