Every language
12 implementations, copy-ready. One at a time with syntax highlighting, or all inline.
JSJavaScript
function throttle(fn, ms) {
let lastRun = 0; // the surviving state
return function throttled(...args) {
const now = Date.now();
if (now - lastRun >= ms) { // window open → fire now (leading)
lastRun = now;
fn(...args);
}
// inside the window: dropped on the floor
};
}
const onScroll = throttle(() => render(), 100);
window.addEventListener('scroll', onScroll);Leading-edge only: the first call of a burst fires and the rest drop — but the final state is lost, because no timer ever runs. The trailing variant stores the latest args and sets a setTimeout to fire once when the window closes; lodash's _.throttle runs both edges (leading + trailing) so the UI responds instantly AND lands the last event, never exceeding one call per interval.
TSTypeScript
export function throttle<F extends (...args: any[]) => void>(
fn: F,
ms: number,
) {
let lastRun = 0; // per-instance: one gate per throttle() call
const throttled = (...args: Parameters<F>) => {
const now = Date.now();
if (now - lastRun >= ms) {
lastRun = now;
fn(...args);
}
};
throttled.cancel = () => { lastRun = Infinity; }; // freeze the gate shut
return throttled;
}Parameters<F> threads the argument types through so call sites stay checked. lastRun lives in the closure, which makes it per-instance state: two throttle(fn, 100) calls yield two independent gates. A timestamp gate has no timer to clear, so cancel() can only freeze it — lastRun = Infinity keeps the window permanently closed.
GoGo
import (
"sync"
"time"
)
type Throttle struct {
mu sync.Mutex
interval time.Duration
last time.Time
}
// allow reports whether this event may run; the winner records its time.
func (t *Throttle) allow() bool {
t.mu.Lock()
defer t.mu.Unlock()
now := time.Now()
if t.last.IsZero() || now.Sub(t.last) >= t.interval {
t.last = now // leading edge: first event always passes
return true
}
return false
}
// scroll storm:
// if t.allow() { render() }The gate is check-and-set under one mutex — compare now.Sub(t.last) and assign t.last in the same critical section, or a concurrent allow() reads a stale window. time.Ticker is the nearby wrong tool: it fires on its own fixed-rate schedule whether or not events arrive, while allow() answers 'may THIS event through?'.
RsRust
use std::sync::Mutex;
use std::time::{Duration, Instant};
struct Throttle {
interval: Duration,
last: Mutex<Option<Instant>>, // None = never fired
}
impl Throttle {
fn new(interval: Duration) -> Self {
Throttle { interval, last: Mutex::new(None) }
}
/// true when this event may run (leading edge).
fn allow(&self) -> bool {
let mut last = self.last.lock().unwrap();
match *last {
Some(t) if t.elapsed() < self.interval => false,
_ => {
*last = Some(Instant::now());
true
}
}
}
}Instant is Rust's monotonic clock — the wall-clock trap other languages warn about is unrepresentable here. tokio::time::interval is the async cousin but runs a task on its own schedule; in async systems throttle usually becomes backpressure — a bounded channel that slows or drops — because 'drop vs wait' is the decision async actually forces.
PHPPHP
// Shared-nothing PHP keeps no closure state between requests, so the
// timestamp lives in a cache keyed by WHAT you are throttling.
function throttled(string $key, callable $fn, int $ms): mixed
{
$now = (int) (microtime(true) * 1000);
$success = false;
$last = apcu_fetch("throttle:{$key}", $success);
if ($success && $now - $last < $ms) {
return null; // inside the window — drop
}
// TTL just above the window: a crashed request cannot wedge the key
apcu_store("throttle:{$key}", $now, intdiv($ms, 1000) + 1);
return $fn();
}
// one email per user per minute:
// throttled("email:{$userId}", fn() => $mailer->send($draft), 60_000);PHP's shared-nothing model makes 'throttle a function' a cache check: the closure's timestamp becomes a shared, keyed, TTL'd value — this is fixed-window rate limiting wearing a function's clothes. APCu is per-server; across a fleet the gate must live in Redis (SET key now NX PX ms). The TTL is the safety net: without it one crashed request pins the window shut forever.
PyPython
import time
def throttle(interval):
"""Leading-edge gate: first call runs, the rest drop until the interval passes."""
last_run = 0.0 # one gate per @throttle line
def decorator(fn):
def wrapped(*args, **kwargs):
nonlocal last_run
now = time.monotonic() # NOT time.time() — wall clock jumps
if now - last_run >= interval:
last_run = now
return fn(*args, **kwargs)
return None
return wrapped
return decorator
@throttle(0.1)
def on_scroll(event):
render(event)time.monotonic(), never time.time(): the wall clock steps backward on NTP sync, and a backward step re-opens a slammed gate (double fire). The decorator form funnels every call of the decorated function through one shared last_run — decorate the sender and the whole event storm passes a single gate.
C#C#
using System;
using System.Threading;
sealed class Throttle
{
private readonly long _intervalMs;
private long _last = long.MinValue; // sentinel: never run
public Throttle(TimeSpan interval) => _intervalMs = (long)interval.TotalMilliseconds;
/// true when this event may run (leading edge).
public bool Allow()
{
long now = Environment.TickCount64; // ms, monotonic
long last = Interlocked.Read(ref _last);
if (last != long.MinValue && now - last < _intervalMs) return false;
// exactly one caller wins each opened window:
return Interlocked.CompareExchange(ref _last, now, last) == last;
}
}Environment.TickCount64 is monotonic milliseconds (DateTime.UtcNow is wall time; Stopwatch.GetTimestamp() for sub-millisecond). The sentinel must be checked explicitly — now - long.MinValue overflows. Interlocked.Read + CompareExchange is the same one-winner-per-window guarantee as a CAS loop, no lock, losers simply get false. The pipeline version is System.Threading.Channels with a bounded channel and FullMode = DropOldest: the consumer's pace becomes the throttle.
JvJava
import java.util.concurrent.atomic.AtomicReference;
class Throttle {
private final long intervalNanos;
private final AtomicReference<Long> last = new AtomicReference<>();
Throttle(long intervalMs) { this.intervalNanos = intervalMs * 1_000_000; }
/** true when this event may run (leading edge). */
boolean allow() {
while (true) {
Long prev = last.get();
long now = System.nanoTime(); // monotonic
if (prev != null && now - prev < intervalNanos) {
return false; // inside the window
}
if (last.compareAndSet(prev, now)) { // one winner per window
return true;
}
// lost the race — re-read and re-check
}
}
}System.nanoTime() is the monotonic clock; System.currentTimeMillis is wall time. The CAS loop makes check-and-set one atomic step: compareAndSet guarantees exactly one thread wins each opened window, losers re-read and usually find it shut. For production rate limiting Guava's RateLimiter (tryAcquire()) is the real thing — and ScheduledExecutorService.scheduleAtFixedRate is fixed-rate periodic work, not event throttling.
SwSwift
import QuartzCore
final class Throttle {
private let interval: Double
private var lastRun = 0.0 // CACurrentMediaTime — monotonic
init(seconds: Double) { interval = seconds }
/// true when this event may run (leading edge). Keep to one thread/actor.
func allow() -> Bool {
let now = CACurrentMediaTime()
guard now - lastRun >= interval else { return false }
lastRun = now
return true
}
}CACurrentMediaTime() is the monotonic clock; Date() is wall time and jumps under NTP. On Apple platforms the framework usually hands you the operator instead: Combine's throttle(for:scheduler:latest:) on a publisher, a sampling Task that loops Task.sleep(for:) and takes the latest value — and render work synced to CADisplayLink is throttled by frame by construction, no timestamp gate needed.
KtKotlin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
// The channel IS the gate: capacity 1 + DROP_OLDEST collapses the storm
// to "the newest unconsumed event"; the collector's own pace is the interval.
val events = Channel<ScrollEvent>(
capacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
// producer (the storm) — never suspends, never throws, drops when full:
// events.trySend(e)
fun CoroutineScope.startHandler() = launch {
for (event in events) {
render(event) // while this runs, newer events replace older ones
}
}A capacity-1 DROP_OLDEST channel plus a collecting loop is throttle by structure: trySend never blocks, the buffer keeps only the newest event, and the consumer's work rate is the interval. On the Flow side the operators ARE throttle semantics — conflate() keeps only the newest value, sample(period) emits at most once per window — reach for those before any hand-rolled timestamp.
RbRuby
def throttle(interval, &block)
last_run = nil
lambda do |*args|
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
if last_run.nil? || now - last_run >= interval
last_run = now
block.call(*args)
end # inside the window: dropped
end
end
on_scroll = throttle(0.1) { |e| render(e) }Process.clock_gettime(Process::CLOCK_MONOTONIC) is the clock that only moves forward — the trap is Time.now, wall time that NTP steps and manual changes move, silently re-opening or wedging a Time-based gate. last_run = nil gives the leading edge: the first event through a fresh throttle always fires.
ZigZig
const std = @import("std");
/// Leading-edge gate: check-and-set one millisecond timestamp under a mutex.
const Throttle = struct {
mutex: std.Thread.Mutex = .{},
last_ms: i64 = 0, // 0 = never ran
interval_ms: i64,
fn allow(t: *Throttle) bool {
t.mutex.lock();
defer t.mutex.unlock();
const now = std.time.milliTimestamp();
if (t.last_ms == 0 or now - t.last_ms >= t.interval_ms) {
t.last_ms = now;
return true;
}
return false;
}
};The mutex exists only to make check-and-set one step: swap the locked field for @atomicLoad(i64, &t.last_ms, .acquire) on the read and @atomicStore(..., .release) on the write and the fast path runs lock-free — at the cost that two threads racing a just-opened window can both fire; @cmpxchgStrong restores exactly-one-per-window. milliTimestamp is epoch-based, not monotonic — fine for UI intervals, wrong where clock steps matter.