TimescaleDB Memory Tuning: shared_buffers, work_mem, and Chunk Sizing
PostgreSQL memory tuning advice is everywhere: set shared_buffers to 25% of RAM, bump work_mem, set effective_cache_size to 75%. That advice is fine for vanilla PostgreSQL. For TimescaleDB, it is incomplete. The chunk-based architecture introduces a constraint that generic PostgreSQL tuning guides never mention: the relationship between chunk sizes and buffer cache capacity.
If your active chunks do not fit in shared_buffers, every other tuning parameter is fighting an uphill battle. This guide covers the four memory settings that matter for TimescaleDB and the chunk sizing math that ties them together.
shared_buffers and the Chunk Size Constraint
shared_buffers is PostgreSQL's dedicated buffer cache -- the shared memory region where table and index pages are cached to avoid disk reads. The standard 25% of system RAM recommendation (4 GB on a 16 GB server) holds for TimescaleDB, but you need to add a constraint: your active chunks must fit in this cache.
An "active chunk" is the chunk currently receiving inserts plus any recent chunks your queries touch frequently. If a dashboard queries the last hour of data from 5 hypertables, each with 1-day chunks, that is 5 active chunks competing for buffer cache space.
The sizing formula:
max_chunk_size < shared_buffers / (N x 2)
Where N is the number of active hypertables and the factor of 2 accounts for indexes (each chunk has its own index copies that also consume cache). For 5 active hypertables with 4 GB of shared_buffers:
4 GB / (5 x 2) = 400 MB per chunk
If your chunks exceed 400 MB, the buffer cache cannot hold all active data simultaneously. Pages get evicted, and subsequent queries on that chunk read from disk. This shows up as a buffer cache hit ratio hovering around 80-90% instead of the 99%+ you should see for recent data.
Diagnosing Oversized Chunks
WITH memory_config AS (
SELECT pg_size_bytes(current_setting('shared_buffers')) AS shared_buffers_bytes
),
chunk_sizes AS (
SELECT
hypertable_name,
chunk_name,
pg_total_relation_size(
format('%I.%I', chunk_schema, chunk_name)
) AS chunk_bytes
FROM timescaledb_information.chunks
WHERE NOT is_compressed
)
SELECT
hypertable_name,
count(*) AS chunk_count,
pg_size_pretty(avg(chunk_bytes)::bigint) AS avg_chunk_size,
pg_size_pretty(max(chunk_bytes)) AS max_chunk_size,
round(max(chunk_bytes)::numeric / shared_buffers_bytes * 100, 1)
AS max_chunk_pct_of_shared_buffers
FROM chunk_sizes, memory_config
GROUP BY hypertable_name, shared_buffers_bytes
ORDER BY max_chunk_pct_of_shared_buffers DESC;
If any hypertable shows chunks exceeding 25% of shared_buffers, reduce the chunk time interval with set_chunk_time_interval(). This only affects future chunks -- existing oversized chunks persist until retention drops them.
work_mem: Per-Operation, Not Per-Connection
work_mem controls how much memory PostgreSQL allocates for each sort, hash join, or hash aggregate operation before spilling to temporary files on disk. The critical detail that catches people: this budget is per operation, not per connection. A single query with three sort nodes can use up to 3x work_mem.
For TimescaleDB, insufficient work_mem hurts in three specific places:
Compression jobs. When TimescaleDB compresses a chunk, it sorts rows by the orderby columns within each segment. Low work_mem forces these sorts to disk, turning a 10-second compression job into a 2-minute one.
Decompression. Reading compressed data requires materializing decompressed rows in memory. Queries that decompress multiple segments in parallel multiply the memory requirement.
Aggregation queries. The typical time-series query -- GROUP BY time_bucket(...) with aggregates -- uses hash aggregation. When the hash table exceeds work_mem, PostgreSQL falls back to a dramatically slower disk-based sort.
The default work_mem of 4 MB is almost always insufficient for TimescaleDB workloads. A reasonable range is 64 MB to 256 MB at the server level. For sessions running heavy aggregations, use SET LOCAL work_mem = '256MB' to increase it temporarily without affecting other connections.
maintenance_work_mem: Fueling Background Jobs
maintenance_work_mem governs memory for maintenance operations: VACUUM, CREATE INDEX, and -- critically for TimescaleDB -- chunk compression background jobs. Each background worker performing compression uses up to maintenance_work_mem for sorting and segment building.
The default of 64 MB is inadequate for any production TimescaleDB deployment. Compression jobs with insufficient memory spill sorts to disk, extending compression duration and increasing I/O load during what should be a lightweight background operation.
Set this to 256 MB minimum, scaling to 1 GB on dedicated servers. Keep in mind that multiple background workers can run simultaneously. With timescaledb.max_background_workers set to 8 and maintenance_work_mem at 512 MB, peak memory consumption from compression alone could reach 4 GB.
SELECT current_setting('maintenance_work_mem') AS maintenance_work_mem;
-- Recommended: ALTER SYSTEM SET maintenance_work_mem = '512MB';
effective_cache_size: A Planner Hint
effective_cache_size does not allocate memory. It tells the query planner how much total cache is available (shared_buffers plus OS page cache), influencing whether it chooses index scans or sequential scans. Set it to 50-75% of total system RAM. On a 16 GB server, 12 GB is appropriate.
Getting this wrong does not cause out-of-memory errors, but it biases the planner toward suboptimal plans -- typically choosing sequential scans when an index scan would be faster, or vice versa.
The Performance Impact Is Dramatic
The gap between correct and incorrect memory configuration:
| Scenario | Buffer Hit Ratio | Compression (1 GB chunk) | Aggregation Query |
|---|---|---|---|
| Well-tuned | 99.2% | 12 seconds | 45 ms |
| Poorly tuned | 82.1% | 3 min 20 sec | 1.8 seconds |
The poorly tuned configuration is not broken -- queries still return correct results. But every operation pays a disk I/O tax that compounds across thousands of queries per hour into visible dashboard latency and slower background job completion.
Memory Tuning Checklist
-
shared_buffers = 25% of system RAM. Verify with
SHOW shared_buffers. -
Chunks fit in the buffer cache. Each chunk should be smaller than
shared_buffers / (N x 2). Reducechunk_time_intervalif oversized. -
work_mem = 64-256 MB. Monitor
temp_blks_writteninpg_stat_statements-- nonzero values mean sorts are spilling to disk. - maintenance_work_mem = 256 MB - 1 GB. Factor in concurrent background workers for peak memory estimation.
- effective_cache_size = 50-75% of RAM. A planner hint, not an allocation.
-
Monitor buffer cache hit ratio. Query
pg_statio_user_tablesfor chunks in_timescaledb_internal. Below 95% on recent chunks indicates memory pressure.
Memory tuning is not a one-time task. As hypertable count grows and ingest rates change, the balance between chunk size and available memory shifts. Revisit these settings whenever you add new hypertables or see performance degradation in your monitoring.
Top comments (0)