Resizing chunk_time_interval on a Live Hypertable

When your ingestion rate shifts and existing chunks drift out of the healthy size band, you can change chunk_time_interval on a running hypertable with a single catalog call — but the new value governs only chunks created after the change, leaving every existing chunk at its original span. This page shows how to profile the gap between your current and target interval, apply the resize safely, and decide whether the already-written chunks need rewriting or can simply age out under retention.

A resize point splits a hypertable timeline into wide historical chunks and narrower future chunks A left-to-right time axis. To the left of a labelled resize point sit three wide chunks written under the old chunk_time_interval; these are unchanged by the resize. To the right sit six narrower chunks written under the new, halved interval. A vertical marker labelled set_chunk_time_interval divides the two regions, showing that only future chunks adopt the new span. Only future chunks adopt the new interval Existing chunks — unchanged old span old span old span set_chunk_time_interval() resize point Future chunks — new interval time →

Why the Interval Falls Out of Band

The chunk_time_interval you chose at create_hypertable time was correct for the ingestion rate you had then. Two things routinely invalidate it: throughput grows as you onboard more devices, or the row footprint changes as you add columns and richer payloads. Either way, chunks written at the old interval start landing outside the 100 MB–1 GB uncompressed sweet spot that keeps TimescaleDB’s background workers, planner, and retention sweeps efficient. The parent guide on time-based chunk partitioning strategies explains why that band matters; this page is about correcting the interval once you are already in production and cannot simply recreate the table.

The critical property to internalise before touching anything: set_chunk_time_interval is a metadata change. It rewrites a single row in the TimescaleDB catalog that describes the time dimension. It does not touch data, does not take a heavy lock, and does not resize, split, or merge any chunk that already exists. The next chunk the ingest path needs to open — for a timestamp beyond the range of every current chunk — is created with the new span. Everything already on disk keeps the span it was born with.

Input Profiling: Current vs Target

Before changing anything, quantify three numbers: the interval in force today, the size the resulting chunks actually reach, and the interval your current ingestion rate implies.

The interval the hypertable stores is an integer count of microseconds. A one-hour interval reads as 3600000000; one day reads as 86400000000. Convert it to something human with an interval cast:

sql
-- Current interval for the time dimension, in microseconds and as an interval.
SELECT
    hypertable_name,
    column_name,
    time_interval                                   AS interval_readable,
    (time_interval)::text                           AS raw_us_or_interval
FROM timescaledb_information.dimensions
WHERE hypertable_name = 'sensor_readings'
  AND column_type IS NOT NULL;

In current TimescaleDB the time_interval column already surfaces as an interval type, but the underlying storage — and what older catalog views such as _timescaledb_catalog.dimension.interval_length expose — is the microsecond integer. Keeping both in view avoids the classic off-by-1000 mistake of reading milliseconds as microseconds.

Now measure what those chunks actually weigh, so you are sizing against reality rather than a launch-day estimate:

sql
-- Observed uncompressed size of the most recent uncompressed chunks.
SELECT
    chunk_name,
    range_start,
    range_end,
    range_end - range_start                         AS span,
    pg_size_pretty(
      pg_total_relation_size(format('%I.%I', chunk_schema, chunk_name))
    )                                               AS chunk_size
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor_readings'
  AND NOT is_compressed
ORDER BY range_start DESC
LIMIT 6;

Linking Interval to Chunk Size

Chunk size scales linearly with the interval at a fixed ingestion rate, which is what makes a resize predictable. A chunk holds one interval’s worth of writes:

Schunk=Iinterval×Rrows/s×BrowS_{chunk} = I_{interval} \times R_{rows/s} \times B_{row}

where SchunkS_{chunk} is the uncompressed chunk size in bytes, IintervalI_{interval} is the interval in seconds, Rrows/sR_{rows/s} is the ingestion rate, and BrowB_{row} is the average bytes per row. Because size is proportional to the interval, correcting an out-of-band chunk is pure arithmetic — to move an observed size SobsS_{obs} onto a target StargetS_{target}, scale the interval by the ratio:

Inew=Iold×StargetSobsI_{new} = I_{old} \times \frac{S_{target}}{S_{obs}}

If your chunks come in at 2 GB and you want the 500 MB midpoint, that ratio is 0.250.25, so quarter the interval. If ingestion has doubled and chunks doubled with it, the ratio is 0.50.5 and you halve the interval. The same reasoning underpins the optimal chunk interval calculation for IoT sensor data; here you are re-running it against changed inputs rather than doing it for the first time.

The Resize Procedure

Applying the new interval is one call. It takes the hypertable and an interval literal, and returns immediately:

sql
-- Change the interval for FUTURE chunks only. No data is moved or locked.
SELECT set_chunk_time_interval('sensor_readings', INTERVAL '30 minutes');

Immediately re-read the dimensions view to confirm the catalog now reports the target span. From this moment, the next chunk opened for an out-of-range timestamp uses the new interval; in-flight chunks that are still accepting rows for their existing range are not retroactively narrowed. If ingestion is continuous, you will typically see the first new-sized chunk appear within one old interval of the change — once the current chunk’s time range fills and the writer rolls over.

When to migrate old data vs let it age out

The resize leaves your historical chunks over- or under-sized. You have two ways to deal with that, and the right one usually depends on your retention horizon:

  • Let it age out (default). If a retention policy will drop these chunks within an acceptable window, do nothing. The out-of-band chunks are transient; every new chunk is correctly sized, and the old ones disappear on schedule. This is the correct choice the large majority of the time — a resize is forward-looking by design.
  • Migrate (recreate and copy). If the mis-sized chunks are far too large, will linger for months, and are actively hurting query planning or retention-drop lock windows, rewrite them. TimescaleDB has no in-place chunk split, so migration means moving the affected range through a fresh path: create a new hypertable with the correct interval, INSERT INTO … SELECT the historical range in batches, then swap. For a bounded window you can also move_chunk/re-insert range-by-range, but a full recreate is simpler to reason about.
sql
-- Migration sketch: recreate with the target interval, copy history in, swap.
CREATE TABLE sensor_readings_resized (LIKE sensor_readings INCLUDING ALL);

SELECT create_hypertable(
    'sensor_readings_resized', 'time',
    chunk_time_interval => INTERVAL '30 minutes'
);

-- Copy in bounded batches so no single statement rewrites the whole history.
INSERT INTO sensor_readings_resized
SELECT * FROM sensor_readings
WHERE time >= '2026-06-01' AND time < '2026-07-01';
-- …repeat per window, then rename the tables to swap in the resized copy.

Migration re-chunks the copied rows at the new interval because they are being inserted, and every insert obeys the interval in force on the destination. Only migrate the ranges that genuinely need it; copying data that retention will drop next week is wasted I/O.

Worked Example: Ingestion Doubles

A hypertable was created at a 1-hour interval when the fleet emitted 2,500 rows/sec at 180 bytes/row, putting each chunk near the top of the band:

Schunk=3600×2500×1801.62 GBS_{chunk} = 3600 \times 2500 \times 180 \approx 1.62 \text{ GB}

That was already high. Six months later the fleet doubled to 5,000 rows/sec, and new 1-hour chunks now weigh about 3.2 GB — well over the ceiling. To pull the size back to the 500 MB midpoint, scale the interval by the ratio 500 MB/3.2 GB0.156500 \text{ MB} / 3.2 \text{ GB} \approx 0.156, which lands on roughly 9.4 minutes; round to a clean boundary and use 15 minutes (or 30 minutes if you accept ~800 MB chunks for fewer catalog rows). Apply it:

sql
SELECT set_chunk_time_interval('sensor_readings', INTERVAL '15 minutes');

The three-plus-GB chunks already on disk keep their 1-hour span. But the very next chunk the ingest path opens covers 15 minutes, and at 5,000 rows/sec × 180 bytes it lands near 810 MB compressed-input size — inside the band. If you watch timescaledb_information.chunks ordered by range_start, you will see the boundary the diagram depicts: a run of wide historical chunks, then a clean switch to quarter-hour spans with no gap and no overlap in coverage.

Edge Cases & When to Deviate

  • Compressed historical chunks. Compressed chunks cannot be resized or re-chunked in place, and you cannot insert freely into a compressed range. To migrate compressed history you must decompress the affected chunks first (decompress_chunk), copy, then recompress — so factor that cost in before choosing migration over aging out. The interaction with columnar compression models for high-frequency telemetry is usually the deciding factor: if old data is already compressed and shrinking well, leave it alone.
  • Space-partitioned hypertables. With a secondary space dimension, each time chunk is further divided per space partition, so the per-chunk footprint is the time-slice size divided by the number of space partitions. Re-run the ratio against the per-partition size, not the aggregate, or you will over-shrink the interval and flood the catalog with tiny chunks.
  • Existing chunks over 1 GB. A single oversized chunk is tolerable; a growing backlog of them is not. If the over-1 GB chunks will persist for months and slow ANALYZE or lengthen retention-drop locks, that is the signal to migrate rather than wait. One or two near the boundary that retention will sweep soon are not worth rewriting.
  • Aligning with retention drop_after. Keep the new interval a clean divisor of your drop_after window so retention keeps dropping whole chunks on tidy boundaries. Switching from 1 hour to 15 minutes preserves a 90-day drop_after alignment; switching to an odd value like 25 minutes does not, and can leave retention straddling partial chunks.
  • Very low new rate. If ingestion fell and the ratio pushes the interval past a day, clamp to 1 day or 7 days rather than letting sparse fleets accrue near-empty chunks.

Verification

Confirm two things after the resize: that new chunks land in the target band, and that old chunks kept their original span.

sql
-- New chunks should carry the target span; old chunks keep the wide span.
SELECT
    chunk_name,
    range_end - range_start                         AS span,
    pg_size_pretty(
      pg_total_relation_size(format('%I.%I', chunk_schema, chunk_name))
    )                                               AS chunk_size,
    is_compressed,
    range_start
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor_readings'
ORDER BY range_start DESC
LIMIT 12;

-- Confirm the catalog now reports the target interval for future chunks.
SELECT hypertable_name, column_name, time_interval
FROM timescaledb_information.dimensions
WHERE hypertable_name = 'sensor_readings';

The span column tells the whole story: chunks created before the resize show the old, wide interval; chunks created after show the new, narrower one — and the time_interval from dimensions matches the new value you set. If recent chunks still show the old span, no roll-over has happened yet because the last pre-resize chunk is still inside its active time range; the next one will switch. If a new chunk still lands out of band, re-profile bytes/row and rows/sec — the ingestion assumptions behind your ratio have moved again.

← Back to Time-Based Chunk Partitioning Strategies · Up to Core Hypertable Architecture & Partitioning Strategy