Understand the lifecycle of a sink

View as Markdown

A sink moves through a sequence of states as it writes data to an external system. Knowing which state a sink is in tells you whether it is making progress or is stuck.

This page covers sinks created with CREATE SINK ... INTO KAFKA and CREATE SINK ... INTO ICEBERG. Both report through the same mz_sink_statuses view and move through the same states, so the queries below apply to either sink type. Differences between the two are called out where they matter.

States

State Meaning
created The default shown before any status has been recorded, cleared almost immediately. See Created, starting, and running.
starting The sink is connecting to the external system and initializing.
running The sink is writing: first its input snapshot, then incremental changes.
paused No cluster replica is running the sink. It makes no progress until one is.
stalled The sink hit an error. The error column reports the cause.
dropped The sink was dropped. Terminal.

The examples below use a Kafka sink on a dedicated cluster. Substitute your own object names.

CREATE CLUSTER export_demo SIZE '25cc';

CREATE CONNECTION kafka_conn TO KAFKA (BROKER 'redpanda:9092', SECURITY PROTOCOL PLAINTEXT);

CREATE SINK orders_sink IN CLUSTER export_demo
  FROM orders
  INTO KAFKA CONNECTION kafka_conn (TOPIC 'orders_sink')
  FORMAT JSON
  ENVELOPE DEBEZIUM;

Created, starting, and running

created is the default mz_sink_statuses reports before any status has been recorded for the sink, not a state a sink lingers in: creating a sink against a running cluster moves it to starting and then running essentially immediately, often within the same reporting interval:

SELECT name, type, status
FROM mz_internal.mz_sink_statuses
WHERE name = 'orders_sink';
    name     | type  | status
-------------+-------+---------
 orders_sink | kafka | running
(1 row)

If the sink’s cluster has no replicas when you create it, it moves to paused instead. See Paused.

running covers both the sink’s initial write of its input snapshot and steady-state writing of incremental changes, so the status alone does not tell you which phase a sink is in. See Hydration and Steady state.

A Kafka connection and a CREATE SINK statement both validate that the target broker or catalog is reachable at statement time, so most connectivity problems surface as an error on the CREATE SINK statement itself rather than a stalled sink. stalled generally means the sink was running and then lost its connection, or the external system rejected a write. See Stalled.

Hydration

  • When: If created WITH (SNAPSHOT = true) (the default), hydrates:

    • On creation, when the sink first emits its input snapshot.
    • On a replica (re)start, but only if the sink restarted before recording any progress: it then re-reads the whole input snapshot, and any data already written to the external system is discarded, but the memory cost still occurs. An established sink resumes from its recorded progress without re-reading the snapshot.
  • What: Loads a full copy of its input snapshot into the arrangement that feeds the sink before it can emit.

  • Memory Use: Peaks at roughly a full copy of the input snapshot, then decreases as the snapshot is written out. Negligible on a restart of an established sink. At steady state, a sink retains little in memory.

Unlike a source snapshot, a sink’s initial write is not separately observable in mz_sink_statistics: the counters described in Steady state accumulate the same way whether the sink is still writing its snapshot or has moved on to incremental changes. To tell whether a sink has finished hydrating, use mz_hydration_statuses instead:

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 = 'orders_sink';
    name     | hydrated
-------------+----------
 orders_sink | t
(1 row)

A sink runs on exactly one cluster replica at a time, even on a cluster with several. replica_id in mz_sink_statistics and mz_sink_status_history identifies that replica; the others sit idle for this sink.

For general hydration behavior, memory impact, and strategies such as a burst replica during hydration, see Hydration.

Steady state

Once the sink is writing steadily, track its progress with mz_sink_statistics:

SELECT messages_staged, messages_committed, bytes_staged, bytes_committed
FROM mz_internal.mz_sink_statistics
WHERE id = (SELECT id FROM mz_sinks WHERE name = 'orders_sink');
 messages_staged | messages_committed | bytes_staged | bytes_committed
-----------------+--------------------+--------------+------------------
               2 |                  2 |           78 |               78
(1 row)

These counters monotonically increase, so compare them over time rather than reading a single snapshot: what matters is that all four keep climbing together. messages_staged/bytes_staged count what the sink has written but not necessarily committed; messages_committed/bytes_committed count what the external system has durably accepted, and can lag behind the staged counters when a write is retried. In practice, a Kafka sink’s transactional writes land in both counters together, so the two rarely differ at query time. The counters reset to zero whenever the sink’s dataflow restarts, for example after stalled recovers, so a drop to 0 on its own does not mean data was lost.

Paused

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

SELECT name, status, details
FROM mz_internal.mz_sink_statuses
WHERE name = 'orders_sink';
    name     | status |                            details
-------------+--------+-----------------------------------------------------------------
 orders_sink | paused | {"hints":["There is currently no replica running this sink"]}
(1 row)

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

Stalled

A sink that hits an error reports stalled, with the cause in error. For example, a Kafka sink whose broker becomes unreachable after the sink was already running:

SELECT name, status, error
FROM mz_internal.mz_sink_statuses
WHERE name = 'orders_sink';
    name     | status  |                                    error
-------------+---------+-------------------------------------------------------------
 orders_sink | stalled | kafka: Transaction error: Timed out waiting for operation to
             |         | finish, retry call to resume
(1 row)

The details column carries a hint alongside the error:

{"hints":["If you're running a single Kafka broker, ensure that the configs
transaction.state.log.replication.factor, transaction.state.log.min.isr, and
offsets.topic.replication.factor are set to 1 on the broker"]}

Common causes are the broker or Iceberg catalog becoming unreachable, a Kafka topic being deleted out from under the sink, or an Iceberg table’s schema changing in a way the sink cannot reconcile with what it is writing. Materialize restarts the sink’s dataflow to retry, so stalled alternates with starting and running in mz_sink_status_history as it does; if the underlying cause has not cleared, the retry fails and the sink stalls again. A cause that will not clear on its own, such as an incompatible schema change on the Iceberg side, needs to be fixed in the external system before the sink can make progress again.

For Iceberg sinks specifically, an unreachable or unhealthy catalog is a common stall cause in practice: Iceberg tables need periodic maintenance (compacting data and delete files, expiring old snapshots) on the catalog side, and a table whose maintenance has fallen behind can start rejecting the sink’s writes even though nothing changed in Materialize.

For causes and fixes, see Troubleshooting sinks.

Dropped

Dropping a sink is terminal. Once dropped, it no longer appears in mz_sink_statuses, but its final dropped status remains in mz_sink_status_history.

Reviewing the full history

mz_sink_statuses reports only the current state. To see every transition a sink has gone through, query the history:

SELECT h.occurred_at, h.status
FROM mz_internal.mz_sink_status_history h
JOIN mz_sinks s ON s.id = h.sink_id
WHERE s.name = 'orders_sink'
ORDER BY h.occurred_at;
        occurred_at         |  status
-----------------------------+----------
 2026-09-11 21:25:14.616+00  | starting
 2026-09-11 21:25:14.616+00  | running
(2 rows)

Unlike a source, which can dwell in starting for a noticeable stretch, a sink typically shows starting and running recorded at essentially the same timestamp, since a sink has no upstream connection step to wait on.

Back to top ↑