Skip to content

関数呼び出しをスロットリングする snippet

スロットルは、イベントが届き続けている間、1インターバルあたり最大1回の呼び出しを保証します。scroll/resize/mousemove 用の道具であり、そこで debounce を使うと、永遠に来ない静寂を待つことになります。落とし穴は debounce との区別です。debounce は静まりの後に発火し、throttle は騒ぎの最中に発火します。実装の形は、タイムスタンプのゲート(leading edge: 先に発火し、インターバルが過ぎるまでブロックする)か、末尾の呼び出しをキューに入れるタイマーです。leading + trailing を組み合わせると、応答は良いが決して N 回を超えない動作になります。

スロットルは、イベントが届き続けている間、1インターバルあたり最大1回の呼び出しを保証します。scroll/resize/mousemove 用の道具であり、そこで debounce を使うと、永遠に来ない静寂を待つことになります。落とし穴は debounce との区別です。debounce は静まりの後に発火し、throttle は騒ぎの最中に発火します。実装の形は、タイムスタンプのゲート(leading edge: 先に発火し、インターバルが過ぎるまでブロックする)か、末尾の呼び出しをキューに入れるタイマーです。leading + trailing を組み合わせると、応答は良いが決して N 回を超えない動作になります。

Runnable recipe · 12 languages
Frontend & DOMthrottletimerseventsrate-limitingscroll

Every language

12 languages, 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.