Cluster memory spike troubleshooting
View as MarkdownA memory spike is a sudden increase in a cluster replica’s memory usage. A spike that outgrows the replica’s heap limit makes Materialize spill to disk, which slows the cluster down, or triggers an out-of-memory (OOM) kill and replica restart. This guide helps you find what caused a spike and how to prevent it recurring.
For sustained CPU/memory pressure and OOM crash loops on an undersized cluster, see Check cluster health. This guide focuses on spikes: sudden increases against an otherwise healthy baseline.
Step 1: Find when the spike happened
Check historical spike
mz_internal.mz_cluster_replica_metrics_history
retains per-replica memory samples across restarts (at least 30 days by
default), so you can find a past spike even if the replica has since
recovered or restarted:
SELECT
c.name AS cluster_name,
r.name AS replica_name,
h.process_id,
max(h.heap_bytes) AS peak_heap_bytes,
max(h.heap_limit) AS heap_limit_bytes,
min(h.occurred_at) FILTER (WHERE h.heap_bytes > 0.9 * h.heap_limit) AS first_above_90pct
FROM mz_internal.mz_cluster_replica_metrics_history h
JOIN mz_catalog.mz_cluster_replicas r ON r.id = h.replica_id
JOIN mz_catalog.mz_clusters c ON c.id = r.cluster_id
WHERE h.occurred_at > now() - INTERVAL '7 days'
GROUP BY c.name, r.name, h.process_id
HAVING max(h.heap_bytes) > 0.9 * max(h.heap_limit)
ORDER BY peak_heap_bytes DESC;
cluster_name | replica_name | process_id | peak_heap_bytes | heap_limit_bytes | first_above_90pct
--------------+--------------+------------+-----------------+-------------------+------------------------
prod_compute | r1 | 0 | 15461882265 | 17179869184 | 2026-09-08 15:49:03+00
(1 row)
Check a live spike
To check whether a replica is under memory pressure right now, query
mz_internal.mz_cluster_replica_metrics:
SELECT
c.name AS cluster_name,
r.name AS replica_name,
m.process_id,
m.heap_bytes,
m.heap_limit,
round(100 * m.heap_bytes::numeric / m.heap_limit, 1) AS heap_pct
FROM mz_internal.mz_cluster_replica_metrics m
JOIN mz_catalog.mz_cluster_replicas r ON r.id = m.replica_id
JOIN mz_catalog.mz_clusters c ON c.id = r.cluster_id
ORDER BY heap_pct DESC;
cluster_name | replica_name | process_id | heap_bytes | heap_limit | heap_pct
--------------+--------------+------------+-------------+-------------+----------
prod_compute | r1 | 0 | 15461882265 | 17179869184 | 90.0
prod_sources | r1 | 0 | 3221225472 | 17179869184 | 18.8
(2 rows)
For sub-minute detail and high-water marks that survive a spike shorter than the sampling interval, see Replica resource usage.
Step 2: Match the spike to a cause
Cross-reference the spike window against these common causes.
A new object hydrated on the cluster
Creating an index, materialized view, or source loads its full result set into memory: hydration. If the new object landed on a cluster that already runs production workloads, its hydration competes with that cluster’s steady-state memory.
Check mz_catalog.mz_audit_events
for create events on the cluster around the spike window:
SELECT occurred_at, event_type, object_type, details
FROM mz_catalog.mz_audit_events
WHERE occurred_at BETWEEN '<spike_start>' AND '<spike_end>'
AND object_type IN ('index', 'materialized-view', 'source')
ORDER BY occurred_at;
See also Check for DDL or deploy activity.
Resolution: Put new objects on their own cluster and cut over with a blue/green deployment instead of adding them to an existing production cluster. See Operational guidelines and Hydration strategies.
An ad-hoc query stood up a dataflow
If a query cannot be served by an existing index or materialized view, a new dataflow is created to serve it. This is similar to hydrating a new index; it causes memory usage to increase. See Dataflows: mental model and basic terminology for background.
Use EXPLAIN PLAN FOR <query> to check whether a query hits an existing index
or builds new operators, then use Dataflow
troubleshooting to identify which
dataflow or operator is consuming memory.
Resolution: Rewrite the query to reuse an existing index, or add one,
rather than relying on ad-hoc plans for hot-path queries. Some query patterns
force a new dataflow even though they look selective, for example a WHERE
clause using <> ALL (...) instead of NOT IN (...).
Many concurrent SUBSCRIBEs restarted at once
Like an ad-hoc query, an active SUBSCRIBE runs as its own dataflow. A cluster
serving many concurrent subscribes (for example, one per connected UI session)
can multiply its restart cost: every subscribe re-establishes its own snapshot
when its replica comes back online, on top of the cluster rebuilding its
indexes. That combined restart cost can exceed what the same subscribes cost
at steady state, and, if it exceeds the replica’s memory limit, produce a
restart loop that looks unrecoverable because every restart recreates the same
spike.
Check how many subscribes are active on a cluster, using
mz_internal.mz_subscriptions:
SELECT count(*) AS active_subscribes
FROM mz_internal.mz_subscriptions s
JOIN mz_catalog.mz_clusters c ON c.id = s.cluster_id
WHERE c.name = '<cluster_name>';
Resolution:
- Add jittered backoff to client reconnect logic so subscribes re-establish gradually instead of all at once.
- As a stopgap, moving the workload to a new cluster lets clients reconnect gradually against already-hydrated indexes, avoiding the simultaneous spike while you fix the reconnect pattern above.
The working set outgrew memory
If a cluster’s data no longer fits in memory, Materialize spills arrangements
to its scratch disk, which shows up as elevated disk_bytes on
mz_cluster_replica_metrics alongside memory near the limit, rather than an
outright OOM. Two common contributors:
- Compaction state. The same source data can require more memory to rehydrate on one replica than another if their upstream collections are compacted to different degrees; a freshly seeded environment can rehydrate cheaper than a long-running one that has retained more history.
- Unnecessarily wide objects. An index or materialized view that carries columns no downstream query uses still arranges them, growing the arrangement for no benefit.
Resolution: Project objects down to only the columns downstream queries need, and use Dataflow troubleshooting: Why is Materialize using so much memory? to find which arrangement is largest.