Sliding vs Tumbling Windows for Sensor Rollups

The choice is not stylistic. A tumbling window partitions time into disjoint intervals, so each reading contributes to exactly one output and each output is an independent statement about a period. A sliding window emits on a schedule shorter than its span, so windows overlap, readings are counted many times, and consecutive outputs are correlated by construction. Both are correct tools; they answer different questions, and using one where the other belongs produces either a regulatory figure you cannot defend or an alert that arrives an hour late. This guide sets out the distinction concretely for windowed aggregation for time-series.

Independence Is the Property That Matters

Ask what a single output value is allowed to be called.

A tumbling one-hour window over 09:00–10:00 produces a value derived only from readings in that hour. It can be published as “the 09:00 hourly mean”, compared against a limit expressed in hourly means, and stored as one row per hour per sensor. Every reading appears in exactly one output, so summing outputs recovers the total, and counting them recovers the coverage.

A sliding one-hour window advancing every five minutes produces twelve values per hour, each sharing 55 minutes of readings with its neighbour. It is excellent at answering “has any rolling hour exceeded the limit?” — the question an alerting system actually asks — and it cannot be published as an hourly series, because eleven of its twelve values describe periods that no reporting standard defines.

The practical consequence is that most environmental pipelines need both, computed from the same stream: tumbling windows to store and publish, sliding windows to alert and display. That is not duplication, it is two different products of the same data.

The same hour, partitioned and overlapped Timeline contrasting tumbling windows that partition the hour into disjoint intervals with sliding windows that overlap, so a single reading contributes to one output in the first case and twelve in the second. The same hour, partitioned and overlapped Tumbling 09:00–10:00 each reading counted once Sliding at 09:05 covers 08:05–09:05 Sliding at 09:10 covers 08:10–09:10 Sliding at 09:15 55 min shared with the last Tumbling 10:00–11:00 independent of the previous window emissions through the hour
Independence is the property that decides publication: only the disjoint windows can be quoted as separate measurements of separate periods.

Production-Ready Implementation

Both, side by side, over the same event-time stream:

# python 3.11 · pandas==2.2.2
import pandas as pd


def tumbling_rollup(df: pd.DataFrame, every: str = "1h") -> pd.DataFrame:
    """Disjoint windows: one row per device per interval, publishable as a series.

    Epoch-aligned, left-closed bins, and a count so a consumer can apply its own
    completeness rule rather than trusting the mean blindly.
    """
    return (
        df.set_index("observed_at")
        .groupby("device_id")
        .resample(every, closed="left", label="left", origin="epoch")
        .agg(mean_value=("value", "mean"),
             max_value=("value", "max"),
             n=("value", "count"))
        .reset_index()
    )


def sliding_rollup(df: pd.DataFrame, span: str = "1h", advance: str = "5min") -> pd.DataFrame:
    """Overlapping windows evaluated every `advance` — for alerting, not publication.

    Each row is the aggregate over the `span` ENDING at that timestamp, so a
    threshold crossing is detected within one advance interval of occurring.
    """
    out = []
    for device_id, group in df.groupby("device_id"):
        s = group.set_index("observed_at")["value"].sort_index()
        rolled = s.rolling(span, closed="left").agg(["mean", "max", "count"])
        sampled = rolled.resample(advance, origin="epoch").last().dropna(how="all")
        out.append(sampled.assign(device_id=device_id).reset_index())
    return pd.concat(out, ignore_index=True)

closed="left" appears in both for the same reason it appears in timestamp resampling: a reading exactly on a boundary must belong to one window, and the choice must be the same everywhere in the pipeline or two stages will disagree about a value at midnight.

In a streaming engine the same distinction is one parameter:

# faust==1.10.4
import faust

app = faust.App("sensor-rollups", broker="kafka://localhost:9092")
readings = app.topic("env.readings.v1", value_type=Reading)

# Tumbling: publishable, one window live per key
hourly = app.Table("hourly_mean", default=WindowAgg).tumbling(
    size=3600.0, expires=timedelta(days=2)
)

# Sliding (hopping): alerting, twelve windows live per key — twelve times the state
rolling_hour = app.Table("rolling_hour", default=WindowAgg).hopping(
    size=3600.0, step=300.0, expires=timedelta(hours=6)
)


@app.agent(readings)
async def aggregate(stream):
    async for reading in stream.group_by(Reading.device_id):
        hourly[reading.device_id] += reading.value
        rolling_hour[reading.device_id] += reading.value

The expires values differ deliberately. Tumbling windows are kept long enough for late telemetry and restatement; sliding windows exist for alerting and can expire as soon as the alert horizon passes, which is what keeps their much larger state from dominating the store.

Live windows per key, and the state that follows Grouped bar chart of live windows per key and estimated state size for four window configurations, showing the overlap factor multiplying both. Live windows per key, and the state that follows 0 20 40 60 tumbling 1 h hop 15 min hop 5 min hop 1 min multiples of the tumbling baseline live windows per key state (100 MB units)
Every overlapping window is a separate live aggregate per key. A one-minute hop over an hour span is sixty times the state of the tumbling equivalent.

Parameter Tuning Guide

Use Window type Span Advance Live windows/key Publishable
Regulatory hourly mean tumbling 1 h 1 h 1 yes
Daily 24 h mean tumbling 24 h 24 h 1 yes
Rolling-hour exceedance alert sliding 1 h 5 min 12 no
Live map (5-minute freshness) sliding 15 min 1 min 15 no
Trend sparkline sliding 6 h 30 min 12 no
Episode detection (gap-closed) session 30 min gap 1 per episode yes, as episodes
Decision Choose tumbling Choose sliding
The output is published or archived
A threshold must be caught within minutes
State budget is tight
Consecutive values must be independent
A human is watching a live chart
The value feeds a downstream statistic

The last row deserves emphasis: feeding overlapping windows into a downstream mean, correlation or interpolation double-counts readings and understates variance, because the inputs are not independent samples.

Which window the output is for Matrix of six uses against the window type each requires and whether its output may be published as a series. Which window the output is for Window Publishable Why Regulatory hourly mean tumbling yes independent periods Rolling-hour exceedance alert sliding no overlaps by design Live map freshness sliding no display only Input to interpolation tumbling yes needs independent samples Trend sparkline sliding no smoothing for the eye Daily archive tumbling yes one row per day
Most pipelines need both from the same stream: tumbling to store and publish, sliding to alert and display.

Verification and Testing

The properties are cleanly testable: tumbling windows must partition, sliding windows must overlap by exactly the expected amount.

# python 3.11 · pandas==2.2.2 · pytest==8.2.0
def test_tumbling_windows_partition_the_readings(readings):
    """Every reading counted exactly once — the property that makes publication valid."""
    out = tumbling_rollup(readings, every="1h")
    assert out["n"].sum() == len(readings)


def test_sliding_windows_overlap_by_the_expected_factor(readings):
    out = sliding_rollup(readings, span="1h", advance="5min")
    # 12 windows per hour, each covering an hour: total counted ~= 12x the readings
    ratio = out["count"].sum() / len(readings)
    assert 10.5 < ratio < 12.5


def test_sliding_window_detects_an_exceedance_within_one_advance(readings_with_spike):
    spike_at = pd.Timestamp("2026-08-01T09:47:00Z")
    out = sliding_rollup(readings_with_spike, span="1h", advance="5min")
    first_alert = out.loc[out["max"] > 150, "observed_at"].min()
    assert (first_alert - spike_at) <= pd.Timedelta("5min")

The first test is the one to keep permanently. A tumbling implementation that double-counts — usually from an off-by-one in the bin edges — passes every eyeball check and fails this assertion immediately.

Gotchas

Publishing a sliding mean as an hourly figure. The headline mistake, and it survives review because the number looks right: a rolling-hour mean and a clock-hour mean differ by a few percent, which reads as noise rather than as a category error.

center=True on a rolling window. It makes the window symmetric around each point, using future readings. In a batch report that may be defensible; in a live pipeline the future rows do not exist, so the same code produces different results offline and online.

Sliding-window state with no expiry. Twelve times the windows and unlimited retention is how a state store reaches double-digit gigabytes in a week. Set expires to just beyond the alert horizon.

Comparing sliding output against a limit defined for a fixed averaging period. Air quality standards specify the averaging period as part of the limit. A rolling-hour value crossing an hourly limit is a useful early warning, not an exceedance — and reporting it as one is a claim you will have to withdraw.


FAQ

Can a sliding window average be published as an hourly mean?

No. Overlapping windows share readings, so consecutive values are not independent measurements of separate periods — quoting one as “the 09:00 hourly mean” attributes a value that includes data from 08:30 to a period it does not cover. Publish tumbling windows and use sliding ones for alerting and display.

How much extra state does a sliding window cost?

The window span divided by the advance interval. A five-minute window advancing every thirty seconds keeps ten windows live per key at any moment, so state is ten times the tumbling equivalent — before accounting for the extra emissions downstream.

Is a rolling window in pandas the same as a sliding window in a stream?

Conceptually yes, operationally no. df.rolling() recomputes over a fixed number of rows or a time span in a completed frame; a streaming sliding window maintains incremental state and emits on a schedule. The pandas version is also happy to use future rows if you set center=True, which is meaningless in a live stream.