# How Listmonk Implements Message Rate Limiting with Sliding Windows

> Discover how Listmonk implements message rate limiting with sliding windows to protect mail servers by tracking counts against time intervals and pausing campaigns when limits are exceeded. Learn the technical details.

- Repository: [Kailash Nadh/listmonk](https://github.com/knadh/listmonk)
- Tags: internals
- Published: 2026-05-19

---

**Listmonk enforces message rate limiting using a sliding window algorithm that tracks message counts against time intervals, pausing the campaign pipeline when limits are exceeded to protect downstream mail servers.**

Listmonk is an open-source newsletter and mailing list manager written in Go. Its campaign processing pipeline supports configurable **message rate limiting with sliding windows** to prevent overwhelming SMTP providers and avoid IP reputation damage. This mechanism is implemented across the configuration models, the campaign manager state, and the subscriber processing pipeline.

## Configuration Settings in models/settings.go

The sliding window behavior is controlled by three fields defined in [`models/settings.go`](https://github.com/knadh/listmonk/blob/main/models/settings.go):

```go
// models/settings.go
AppMessageSlidingWindow         bool   `json:"app.message_sliding_window"`
AppMessageSlidingWindowDuration string `json:"app.message_sliding_window_duration"`
AppMessageSlidingWindowRate     int    `json:"app.message_sliding_window_rate"`

```

These settings are loaded into the manager's `Config` struct in [`internal/manager/manager.go`](https://github.com/knadh/listmonk/blob/main/internal/manager/manager.go) as strongly typed values:

```go
type Config struct {
    // …
    SlidingWindow         bool
    SlidingWindowDuration time.Duration
    SlidingWindowRate     int
}

```

When the application initializes, these values determine whether the sliding window limiter activates and define the specific time window and message cap.

## Manager State Tracking

The campaign manager maintains sliding window state through two fields declared in [`internal/manager/manager.go`](https://github.com/knadh/listmonk/blob/main/internal/manager/manager.go):

```go
// internal/manager/manager.go
slidingCount int          // messages sent in the current window
slidingStart time.Time    // start time of the current window

```

During manager initialization (`New`), the `slidingStart` timestamp is set to the current time:

```go
slidingStart: time.Now(),

```

These fields track the rolling count of messages dispatched within the active window duration, allowing the system to enforce rate limits without external dependencies.

## Pipeline Enforcement in pipe.go

The actual throttling logic resides in [`internal/manager/pipe.go`](https://github.com/knadh/listmonk/blob/main/internal/manager/pipe.go) within the `NextSubscribers` function, which fetches batches of subscribers for active campaigns. The implementation first checks if sliding window mode is enabled:

```go
// internal/manager/pipe.go
hasSliding := p.m.cfg.SlidingWindow &&
               p.m.cfg.SlidingWindowRate > 0 &&
               p.m.cfg.SlidingWindowDuration.Seconds() > 1

```

When `hasSliding` evaluates to true, the pipeline examines each message before queuing:

```go
if hasSliding {
    diff := time.Since(p.m.slidingStart)

    // Reset the window when the configured duration has elapsed.
    if diff >= p.m.cfg.SlidingWindowDuration {
        p.m.slidingStart = time.Now()
        p.m.slidingCount = 0
    }

    // Increment the counter for this window.
    p.m.slidingCount++

    // If the count exceeds the configured limit, pause processing.
    if p.m.slidingCount >= p.m.cfg.SlidingWindowRate {
        wait := p.m.cfg.SlidingWindowDuration - diff
        p.m.log.Printf(
            "messages exceeded (%d) for the window (%v since %s). Sleeping for %s.",
            p.m.slidingCount,
            p.m.cfg.SlidingWindowDuration,
            p.m.slidingStart.Format(time.RFC822Z),
            wait.Round(time.Second)*1,
        )
        // Reset the counter and sleep for the remaining window time.
        p.m.slidingCount = 0
        time.Sleep(wait)
    }
}

```

## How the Sliding Window Algorithm Works

### Window Reset Logic

When the elapsed time since `slidingStart` (`diff`) exceeds the configured `SlidingWindowDuration`, the manager resets both the start timestamp and message counter. This creates a fresh time window and ensures that rate calculations always reflect the most recent interval rather than accumulating indefinitely.

### Limit Breach Handling

If `slidingCount` reaches `SlidingWindowRate` before the window expires, the manager calculates the remaining time in the current window (`wait`) and invokes `time.Sleep(wait)`. This blocks the `NextSubscribers` loop entirely, preventing additional messages from entering the send queue until the window slides forward. Because this sleep occurs within the message-fetch loop, the campaign effectively pauses rather than dropping messages or failing.

## Interaction with Non-Sliding Rate Limiting

Listmonk also supports a simpler per-minute rate limiter implemented via `ratecounter.RateCounter`, instantiated in [`pipe.go`](https://github.com/knadh/listmonk/blob/main/pipe.go) as `ratecounter.NewRateCounter(time.Minute)`. However, when the sliding window option is enabled (`hasSliding == true`), the per-minute counter is instantiated but not consulted for throttling decisions. The sliding window implementation takes precedence and provides finer-grained control over burst behavior compared to the rolling minute counter.

## Configuration Examples

### Enabling Sliding Window via YAML

To activate the limiter in your configuration file:

```yaml

# config.yml

app:
  message_sliding_window: true
  message_sliding_window_duration: 1m   # 1 minute window

  message_sliding_window_rate: 500      # max 500 messages per window

```

### Programmatic Configuration

When creating a `Manager` instance directly:

```go
import (
    "time"
    "github.com/knadh/listmonk/internal/manager"
    "github.com/knadh/listmonk/internal/i18n"
    "log"
    "os"
)

func main() {
    cfg := manager.Config{
        BatchSize:             1000,
        Concurrency:           5,
        SlidingWindow:         true,
        SlidingWindowDuration: time.Minute, // 1‑minute window
        SlidingWindowRate:     300,         // 300 msgs per minute
    }

    // Assume `store` implements manager.Store and `i18n` is set up.
    mgr := manager.New(cfg, store, i18n.New(), log.New(os.Stdout, "", log.LstdFlags))
    
    // The manager now enforces sliding window limits automatically.
}

```

### Observing Throttling in Logs

When limits are breached, Listmonk emits log entries similar to:

```

2026/05/19 10:15:42 messages exceeded (500) for the window (1m0s since Mon, 19 May 2026 10:14:42 +0000). Sleeping for 30s.

```

This output originates from the `log.Printf` call in [`internal/manager/pipe.go`](https://github.com/knadh/listmonk/blob/main/internal/manager/pipe.go) and confirms the pipeline is actively throttling to respect the sliding window configuration.

## Summary

- **Configuration**: Three settings in [`models/settings.go`](https://github.com/knadh/listmonk/blob/main/models/settings.go) control the feature: `app.message_sliding_window`, `app.message_sliding_window_duration`, and `app.message_sliding_window_rate`.
- **State Management**: The manager tracks window state via `slidingCount` and `slidingStart` fields initialized in [`internal/manager/manager.go`](https://github.com/knadh/listmonk/blob/main/internal/manager/manager.go).
- **Enforcement**: Logic in [`internal/manager/pipe.go`](https://github.com/knadh/listmonk/blob/main/internal/manager/pipe.go) checks limits inside `NextSubscribers`, blocking the pipeline with `time.Sleep()` when rates exceed configured thresholds.
- **Burst Prevention**: The algorithm resets counters automatically when windows expire and prevents boundary bursts by sleeping through the remainder of exceeded windows.
- **Precedence**: When enabled, the sliding window limiter overrides the simpler per-minute `RateCounter` mechanism.

## Frequently Asked Questions

### What is the difference between sliding window and fixed window rate limiting?

**Sliding window rate limiting** tracks message counts against a rolling time interval that moves continuously forward, preventing burst attacks at window boundaries that can occur with fixed windows. Listmonk's implementation checks elapsed time since `slidingStart` and resets the counter only after the full duration passes, ensuring no fixed reset points exist where bursts could accumulate.

### How do I configure message rate limiting with sliding windows in Listmonk?

Set the three configuration parameters in your [`config.yml`](https://github.com/knadh/listmonk/blob/main/config.yml) file or environment variables: enable `app.message_sliding_window` (boolean), define `app.message_sliding_window_duration` as a Go duration string (e.g., `"1m"`, `"5m"`), and set `app.message_sliding_window_rate` to the integer maximum messages allowed within that duration. These values map to the `Config` struct fields consumed by the campaign manager.

### What happens when a campaign exceeds the configured sliding window rate?

When `slidingCount` reaches `SlidingWindowRate`, the manager calculates the remaining time in the current window and executes `time.Sleep(wait)` inside the `NextSubscribers` loop. This pauses campaign processing entirely, logs the throttling event with timestamp details, and resumes only after the window slides forward. Messages are not dropped; the pipeline simply delays further sending.

### Does sliding window rate limiting work with concurrent campaign processing?

Yes, but the sliding window state (`slidingCount` and `slidingStart`) is maintained per manager instance. If you run multiple manager instances or scale horizontally, each instance tracks its own window independently. For strict global rate limiting across multiple instances, you would need to implement external coordination or reduce the per-instance `SlidingWindowRate` proportionally.