Sliding Window Limiter

Track request timestamps in a sliding window to enforce a precise per-interval quota, with an injectable clock for tests.

GoResilience

Code

snippet
package main

import (
	"fmt"
	"sort"
	"sync"
	"time"
)

type SlidingWindowLimiter struct {
	mu     sync.Mutex
	limit  int
	window time.Duration
	now    func() time.Time
	events []time.Time
}

func NewSlidingWindowLimiter(limit int, window time.Duration, now func() time.Time) *SlidingWindowLimiter {
	if limit <= 0 || window <= 0 {
		panic("limit and window must be positive")
	}
	if now == nil {
		now = time.Now
	}
	return &SlidingWindowLimiter{limit: limit, window: window, now: now}
}

func (l *SlidingWindowLimiter) prune(current time.Time) []time.Time {
	cutoff := current.Add(-l.window)
	first := sort.Search(len(l.events), func(i int) bool {
		return l.events[i].After(cutoff)
	})
	l.events = append(l.events[:0], l.events[first:]...)
	return l.events
}

func (l *SlidingWindowLimiter) Allow() bool {
	l.mu.Lock()
	defer l.mu.Unlock()
	current := l.now()
	active := l.prune(current)
	if len(active) >= l.limit {
		return false
	}
	l.events = append(active, current)
	return true
}

func (l *SlidingWindowLimiter) Remaining() int {
	l.mu.Lock()
	defer l.mu.Unlock()
	active := l.prune(l.now())
	if remaining := l.limit - len(active); remaining > 0 {
		return remaining
	}
	return 0
}

func main() {
	clock := time.Date(2026, 10, 21, 12, 0, 0, 0, time.UTC)
	now := func() time.Time { return clock }
	limiter := NewSlidingWindowLimiter(3, time.Second, now)

	for i := 0; i < 4; i++ {
		clock = clock.Add(100 * time.Millisecond)
		fmt.Printf("t=%dms allow=%v remaining=%d\n", (i+1)*100, limiter.Allow(), limiter.Remaining())
	}

	clock = clock.Add(700 * time.Millisecond)
	fmt.Println("after window slides:", limiter.Allow())
}