Understand the lifecycle of a source

View as Markdown

A source and the tables created from it move through a sequence of states before they continuously serve up-to-date data. Knowing which state an object is in tells you whether it is making progress or is stuck.

This page covers sources created with the CREATE SOURCE and CREATE TABLE ... FROM SOURCE syntax. All source types report through the same mz_source_statuses view and move through the same states, so the queries below apply whether you ingest from Kafka, PostgreSQL, MySQL, SQL Server, or a load generator.

The source and each of its tables report their own status. The source tracks the connection to the upstream system, while each table tracks the ingestion of one upstream relation.

States

State Meaning
created The object exists but is not ingesting yet.
starting The object is connecting to the upstream system and initializing.
running The object is ingesting: first the initial snapshot, then upstream changes.
paused No cluster replica is running the object. It makes no progress until one is.
stalled The object hit an error. The error column reports the cause.
dropped The object was dropped. Terminal.

stalled covers both errors that Materialize retries on its own and errors that do not clear until you act. See Stalled.

The examples below use a PostgreSQL source and a Kafka source on a dedicated cluster. Substitute your own object names.

CREATE CLUSTER ingest_demo SIZE '25cc';

CREATE SOURCE pg_src IN CLUSTER ingest_demo
  FROM POSTGRES CONNECTION pg_conn (PUBLICATION 'mz_orders');

CREATE SOURCE kafka_src IN CLUSTER ingest_demo
  FROM KAFKA CONNECTION kafka_conn (TOPIC 'clicks');

Created

A source that has no tables yet reports created. At this point Materialize has recorded the source and its upstream connection, but it is not ingesting anything and consumes no cluster resources:

SELECT name, type, status
FROM mz_internal.mz_source_statuses
ORDER BY name;
   name    |   type   | status
-----------+----------+---------
 kafka_src | kafka    | created
 pg_src    | postgres | created
(2 rows)

A source stays in created for as long as it has no tables. Ingestion begins only when you attach a table with CREATE TABLE ... FROM SOURCE:

CREATE TABLE orders FROM SOURCE pg_src (REFERENCE orders);
CREATE TABLE clicks FROM SOURCE kafka_src (REFERENCE clicks) FORMAT JSON;

Starting and running

Once a table is attached, the source and the table connect to the upstream system (starting), then begin ingesting (running):

SELECT o.name, s.status, s.error
FROM mz_internal.mz_source_statuses s
JOIN mz_objects o ON o.id = s.id
ORDER BY o.name;
   name    | status  | error
-----------+---------+-------
 clicks    | running |
 kafka_src | running |
 orders    | running |
 pg_src    | running |
(4 rows)

starting is usually brief. If an object stays in starting for more than a few minutes, see Troubleshooting: Why isn’t my source ingesting data?.

Snapshotting

running covers both the initial snapshot and steady-state ingestion, so the status alone does not tell you whether the initial snapshot is still in progress. Use mz_source_statistics instead:

SELECT o.name, s.snapshot_records_known, s.snapshot_records_staged,
       s.snapshot_committed
FROM mz_internal.mz_source_statistics s
JOIN mz_objects o ON o.id = s.id
WHERE o.name IN ('orders', 'clicks')
ORDER BY o.name;
  name  | snapshot_records_known | snapshot_records_staged | snapshot_committed
--------+------------------------+-------------------------+--------------------
 clicks | 200                    | 200                     | t
 orders | 501                    | 501                     | t
(2 rows)

While the snapshot is in progress, snapshot_records_staged climbs toward snapshot_records_known and snapshot_committed is f. A table cannot serve queries until its snapshot completes: queries against it block until then.

Nothing is committed until the whole snapshot has been read, because Materialize ingests it at a single timestamp. So offset_committed does not advance for the duration, and on upsert sources even updates_staged sits at 0, because the source buffers the snapshot while it builds its in-memory index. In those statistics a snapshot that is progressing normally is indistinguishable from a stuck one, so read progress from snapshot_records_staged, messages_received, and bytes_received.

NOTE: These statistics are collected periodically, so for a window after an object starts running they can read NULL and f even though the data is already ingested and queryable. They also reset when a replica restarts. Track how they evolve rather than reading them at a single moment.

mz_hydration_statuses answers the same question one level up, per source and replica rather than per table:

SELECT o.name, h.hydrated
FROM mz_internal.mz_hydration_statuses h
JOIN mz_objects o ON o.id = h.object_id
WHERE o.name IN ('pg_src', 'kafka_src')
ORDER BY o.name;
   name    | hydrated
-----------+----------
 kafka_src | t
 pg_src    | t
(2 rows)

hydrated is false while any table attached to the source is still snapshotting, whatever the source type, which makes it a cheap source-level check. It is not specific to the initial snapshot: a replica restart resets it too, after which it tracks the source rebuilding its in-memory state.

Snapshot duration and upstream impact vary by source type. CDC sources (PostgreSQL, MySQL, SQL Server) require the upstream system to retain its change log until the snapshot completes, so a long snapshot increases upstream disk usage. Kafka sources have no equivalent retention requirement. See Snapshotting and Monitoring the snapshotting progress.

Steady state

Once the snapshot is committed, the object continually ingests upstream changes and status stays running. To confirm it is keeping up, compare the offset Materialize has committed against the offset it knows about upstream:

SELECT o.name, s.offset_known, s.offset_committed,
       s.offset_known - s.offset_committed AS offset_delta
FROM mz_internal.mz_source_statistics s
JOIN mz_objects o ON o.id = s.id
WHERE o.name IN ('orders', 'clicks')
ORDER BY o.name;
  name  | offset_known | offset_committed | offset_delta
--------+--------------+------------------+--------------
 clicks | 200          | 200              | 0
 orders | 22526904     | 22526904         | 0
(2 rows)

You want offset_delta close to 0. The unit depends on the source type: for Kafka sources an offset is a Kafka offset, while for PostgreSQL sources it is a log sequence number (LSN), which is why the two rows above differ by orders of magnitude. Compare each object against itself over time rather than against other objects. See Monitoring data lag.

NOTE: A cluster replica restart or resize triggers hydration. For Kafka upsert sources, this rebuilds the table’s internal upsert index from storage; for other source types, hydration is negligible or not applicable.

Paused

An object whose cluster has no replicas reports paused and makes no progress. The details column reports why:

SELECT o.name, s.status, s.details
FROM mz_internal.mz_source_statuses s
JOIN mz_objects o ON o.id = s.id
ORDER BY o.name;
   name    | status |                             details
-----------+--------+-----------------------------------------------------------------
 clicks    | paused | {"hints":["There is currently no replica running this source"]}
 kafka_src | paused | {"hints":["There is currently no replica running this source"]}
 orders    | paused | {"hints":["There is currently no replica running this source"]}
 pg_src    | paused | {"hints":["There is currently no replica running this source"]}
(4 rows)

A cluster that had a replica and lost it reports a different hint, The replica running this source has been dropped. Either way, ingestion resumes when the cluster has a replica again, so increase the replication factor of the cluster hosting the source.

Stalled

An object that hits an error reports stalled, with the cause in error:

SELECT o.name, s.status, s.error
FROM mz_internal.mz_source_statuses s
JOIN mz_objects o ON o.id = s.id
ORDER BY o.name;

In the output below, the publication backing the PostgreSQL source was dropped upstream. Both pg_src and its table orders stall, because neither can make progress without it, while the unrelated Kafka source keeps running:

   name    | status  |                      error
-----------+---------+--------------------------------------------------
 clicks    | running |
 kafka_src | running |
 orders    | stalled | postgres: publication "mz_orders" does not exist
 pg_src    | stalled | postgres: publication "mz_orders" does not exist
(4 rows)

Sources stall independently of one another, so a stall is scoped to the source that hit the error and the tables that depend on it.

The details column carries the same error tagged with the subsystem that reported it, which tells you which part of the pipeline failed:

{"namespaced":{"postgres":"publication \"mz_orders\" does not exist"}}

Which stalls clear on their own

Errors unrelated to the ingested data, such as a connection failure, an authentication failure, or an upstream restart, are retried. Materialize restarts the ingestion dataflow when it needs to, and the object moves back through starting to running once the upstream problem clears. Repeated stalled and starting transitions in mz_source_status_history are the signature of an error being retried.

Other errors are definite: the upstream system changed in a way that invalidates what Materialize has already ingested. Dropping the publication a PostgreSQL source replicates from, dropping or truncating an upstream table, invalidating a replication slot, and an incompatible upstream schema change all land here. Materialize records a definite error durably against the affected table, so reads of that table return it and restarting the dataflow does not clear it. Recovery means fixing the upstream cause and recreating the affected tables, as in Absorbing upstream schema changes. The stall above is one of these, so orders cannot be repaired in place.

For causes and fixes, see Troubleshooting data ingestion for any source type, and the CDC-specific guides for PostgreSQL and MySQL, which cover replication slot, WAL, and GTID errors unique to those connectors.

Dropped

Dropping an object is terminal. Once dropped, it no longer appears in mz_source_statuses, but its final dropped status remains in mz_source_status_history.

Reviewing the full history

mz_source_statuses reports only the current state. To see every transition an object has gone through, which is the fastest way to understand how it reached its current state, query the history:

SELECT o.name, h.occurred_at, h.status
FROM mz_internal.mz_source_status_history h
JOIN mz_objects o ON o.id = h.source_id
WHERE o.name IN ('orders', 'clicks')
ORDER BY h.occurred_at;
  name  |        occurred_at         |  status
--------+----------------------------+----------
 orders | 2026-09-11 15:04:24.384+00 | starting
 orders | 2026-09-11 15:04:24.385+00 | running
 clicks | 2026-09-11 15:04:24.515+00 | starting
 clicks | 2026-09-11 15:04:24.56+00  | running
(4 rows)

created is not a recorded transition, so it never appears in the history. Here both tables moved from starting to running within milliseconds, since each had only a few hundred rows to snapshot.

Back to top ↑