RisingWave vs Materialize vs Flink SQL: Streaming SQL for IoT Telemetry
A plant historian answers the question “what was the average bearing temperature last hour?” by scanning stored data every time someone asks. A fleet of ten thousand sensors reporting once a second produces 864 million readings per day, and a dashboard that rescans them on every refresh either burns money or goes stale. Streaming SQL flips the model: you write the query once, the engine keeps its answer current as each reading arrives, and readers fetch a precomputed result in milliseconds.
Three engines dominate this conversation in 2026, and they make very different bets. RisingWave stores state in object storage and speaks the PostgreSQL wire protocol. Materialize maintains views with incremental computation over in-memory arrangements and offers strict consistency. Apache Flink SQL is the general-purpose workhorse with the deepest connector and windowing toolbox, and a state backend that is itself being rebuilt for the cloud.
This post gives you the mechanisms, the SQL for the same telemetry rollup in each engine, and a decision matrix you can defend in a design review.
What this covers: how incremental view maintenance works, how each engine stores state and handles consistency, windows, late data and expiry, runnable rollup SQL for all three, the failure modes that bite IoT workloads, and when to pick which.
Context and Background
Stream processing for telemetry has traditionally meant a pipeline of three things: a broker, a stream processor that computes rollups, and a serving database that dashboards query. The processor wrote its output into a time-series store or a key-value cache, and the serving layer was a separate system with its own schema, its own consistency story, and its own on-call rotation. Our earlier comparison of Flink, Spark Structured Streaming and Kafka Streams covers that classic split in detail, and the broker side is treated in the Kafka vs Redpanda vs WarpStream ADR and the NATS JetStream vs Kafka edge comparison.
The newer idea is to collapse the processor and the serving layer into one SQL system. You declare a materialized view over a stream, the database keeps it fresh, and you query it with any PostgreSQL driver. The technique underneath is called incremental view maintenance: instead of recomputing a query from scratch when inputs change, the engine computes only the delta, the change to the output caused by the change to the input. A new temperature reading for device 17 touches one aggregate row, not the whole table.
RisingWave and Materialize are built around that idea from the first line of code. Flink approached from the other side. It began as a dataflow engine with a SQL layer on top, and it has been moving toward the same destination. Flink 2.0 shipped on March 24, 2025 as the first major release since 1.0, and it introduced Materialized Tables, a table type defined by a query and a freshness target (see the Flink 2.0 announcement). Flink 2.2 added ML_PREDICT and VECTOR_SEARCH, which we cover in Flink 2.2 ML_PREDICT and vector search for streaming AI.
Licensing matters when you build a platform on one of these, so we checked it. RisingWave’s repository is licensed under Apache 2.0. Materialize’s documentation states that both Materialize Cloud and the Self-Managed Community edition are governed by the Business Source License, and that self-managed deployments need a license key; its repository describes the engine source as BSL 1.1 converting to Apache 2.0 after four years. Flink is an Apache Software Foundation project under the Apache license. The practical reading is simple: two engines are open source in the OSI sense, and one is source-available with commercial terms you should read before you embed it in a product.
One caution about everything below. Version numbers, defaults and syntax change quickly in this category. Where we cite a default or a feature, we cite the vendor documentation as read on the date of writing, and we label what we could not verify. Throughout, any sizing figure is an illustrative calculation, not a benchmark.
Reference Architecture: Where Streaming SQL Sits in a Telemetry Stack
Direct answer: A streaming SQL engine reads sensor events from a broker such as Kafka, maintains materialized views incrementally using persisted operator state, and serves those views to dashboards and APIs directly or through a sink. It replaces the separate stream processor plus serving database with one system that you query using ordinary SQL.

Figure 1: Reference architecture for streaming SQL over IoT telemetry, with the engine sitting between the broker and the consumers.
Figure 1 shows the shape that all three engines share. Edge gateways publish readings to a broker. The engine ingests them, assigns event-time watermarks, and maintains one or more layers of materialized views: a cleaned and enriched layer, a windowed rollup layer, and a latest-state layer that acts as a digital twin projection. Consumers either query the engine directly over the PostgreSQL wire protocol or receive changes pushed to a sink such as Kafka, Iceberg or a time-series database. The engines differ in how much of the right-hand side they own.
Incremental view maintenance in one worked example
Take the query “average temperature per device per minute.” A batch database would scan every row. An incremental engine stores, for each open window and device, a running sum and count. When a reading of 71.5 degrees arrives for device 17 in the 09:41 window, the engine looks up that one state row, adds 71.5 to the sum, increments the count, and emits the change to the aggregate output as a retraction of the old average plus an insertion of the new one. Work per event is constant regardless of how much history exists.
That is the central economic fact. At ten thousand events per second, a per-event cost that is O(1) in the number of open keys is affordable. A design that does work proportional to table size is not. Every difference between the three engines can be understood as a different answer to two questions: where does the per-key state live, and what guarantees do readers get about the combined result?
Three answers to “where does state live”
RisingWave keeps persistent state in object storage. Its architecture documentation describes four node types: serving nodes that run the PostgreSQL-compatible frontend, streaming nodes that run the jobs and maintain state, a meta node that coordinates scheduling, checkpointing and recovery, and compactor nodes that compact the LSM-tree storage in the background. All persistent data lives in S3, GCS or Azure Blob, organised in levels from L0 upward, so compute and storage scale independently. The repository README adds that an elastic disk cache can keep hot data on local SSD and that this is meant to hold p99 query latency in the 10 to 20 millisecond range; we treat that as a vendor claim, not a measured result.
Materialize’s concepts documentation describes clusters as isolated pools of compute that run sources, sinks, indexes and materialized views, and describes arrangements as the in-memory data structures that maintain indexes and materialized views. When a cluster restarts it goes through hydration, rebuilding in-memory state from Materialize’s storage layer without re-reading the upstream system. The design philosophy is that the working set lives in memory and storage is the durable backing. Materialize is widely documented as being built on the Timely and Differential Dataflow libraries; the concepts page we fetched does not name them, so we flag that attribution as background knowledge rather than something we verified on that page.
Flink holds state in a state backend, traditionally RocksDB on local disk with periodic checkpoints to a distributed file system. Flink 2.0 added disaggregated state management, in which a distributed file system becomes the primary state store. The new ForSt backend (the name stands for “For Streaming”) separates state from compute so local disk no longer caps state size, and an asynchronous execution model allows many state lookups to be in flight at once. The flag table.exec.async-state.enabled switches on async state access for supported SQL operators.
What the serving contract looks like
The second difference is how readers see the output. RisingWave and Materialize both present a query surface directly: you connect with psql or a JDBC driver and SELECT from a materialized view. Flink does not serve queries to dashboards by itself. The usual pattern is to write the results to a sink, such as an upsert Kafka topic, a JDBC database, Paimon or Iceberg, and let another system answer queries. Flink’s Materialized Tables narrow that gap by managing refresh and freshness declaratively, but they sit on top of a lakehouse-style table format, not a built-in serving index.

Figure 2: State and serving paths compared, with RisingWave and Flink moving state to object storage while Materialize holds arrangements in memory.
The long description of Figure 2: the three engines are drawn as parallel lanes from broker to reader. In the RisingWave lane, streaming nodes write to a Hummock-style LSM on object storage and serving nodes answer queries. In the Materialize lane, a cluster holds arrangements in memory with durable storage behind it. In the Flink lane, task managers use a state backend, with a sink between the job and whatever serves readers.
Deeper Analysis: Consistency, Windows, Late Data, and the Same Rollup Three Ways
Consistency semantics
Consistency is where the engines are easiest to misjudge, because the marketing words overlap. Here is what each vendor documents.
RisingWave achieves consistency through barriers. The meta node injects barriers, periodic synchronisation markers, into the streams; the documented default interval is one second. When every streaming node has processed a barrier and uploaded its state, the checkpoint completes and produces a globally consistent snapshot. Recovery restarts the actors from the last checkpoint. The practical consequence is that a materialized view and the views upstream of it are updated together as of a barrier, so you do not read a rollup that disagrees with its own source table. We did not find a formal isolation-level statement on the pages we read, so we will not name one for RisingWave.
Materialize documents its isolation levels explicitly. Strict Serializable is the default and provides serializability plus linearizability: the serial order matches real time, and once data appears in a read, later reads will see it. Reads may wait briefly for recent writes to propagate through materialized views and indexes. Serializable relaxes the real-time guarantee so reads do not wait, at the cost of slight staleness. A Bounded Staleness mode serves reads at a timestamp no older than a chosen duration and returns an error rather than block. Importantly, the documentation notes that linearizability covers transactions, not data still being ingested from upstream; a separate real-time recency feature, in private preview at the time we read it, addresses that.
Flink’s guarantee is different in kind. Flink checkpoints give exactly-once state semantics inside the job, but the job’s output reaches the outside world through a sink, so end-to-end behaviour depends on the sink. An upsert sink is idempotent and tolerates replays; an append-only sink needs a transactional two-phase commit to avoid duplicates. There is no cross-view read consistency to speak of, because there is no built-in read path.
For telemetry dashboards this distinction is mostly academic until it is not. If an operator sees a “line stopped” alarm view and a “last known good reading” view that disagree by thirty seconds, they lose trust in both. Materialize and RisingWave give you a defensible story about reading several views together; with Flink you design that story yourself at the sink.
Windowing: the vocabulary of IoT rollups
Telemetry rollups are window queries: per-minute averages, five-minute maxima, sessions of machine activity. All three engines support the same core window types, with differences in syntax and mode.
Flink SQL uses windowing table-valued functions. The documentation lists TUMBLE(TABLE data, DESCRIPTOR(timecol), size [, offset]), HOP(TABLE data, DESCRIPTOR(timecol), slide, size [, offset]), CUMULATE(TABLE data, DESCRIPTOR(timecol), step, size) and SESSION(TABLE data [PARTITION BY (keycols, ...)], DESCRIPTOR(timecol), gap). Each adds window_start, window_end and window_time columns. CUMULATE is the underrated one for IoT: windows share a start time and grow by a step up to a maximum size, which gives you a “shift to date” total that updates through the shift.
RisingWave supports tumble and hop through TUMBLE(table_or_source, time_col, window_size [, offset]) and HOP(table_or_source, time_col, hop_size, window_size [, offset]). Its documentation notes that hop output has N times as many rows as the input, where N is window size divided by hop size. That matters for sizing: a 10-minute window hopping every 30 seconds gives N = 20, so each reading is counted in 20 windows. Session windows use a window function frame such as SESSION WITH GAP INTERVAL '5 MINUTES' and, per the docs, are supported only in batch mode and emit-on-window-close streaming mode.
Materialize takes a different route. It has no windowing table function in the pages we read; instead it expresses time bounds with temporal filters built on mz_now(), combined with date_bin to bucket timestamps. A temporal filter such as mz_now() <= event_ts + INTERVAL '30s' keeps only events from the last 30 seconds, and records that stop satisfying the condition are retracted, which bounds memory. The documentation shows a pattern that emits each one-minute count once, at window end, and keeps it for seven days, using three cooperating filters. It is expressive and precise, and it is also a more manual way to say “tumbling window.”
Late data and state expiry
Sensors lie about time. A gateway that loses its uplink buffers readings and flushes them an hour later. A PLC clock drifts. This is where streaming SQL engines show their philosophy.
In Flink, you declare WATERMARK FOR event_time AS event_time - INTERVAL '30' SECOND on the source table; window results are emitted when the watermark passes the window end, and rows later than that are dropped by window operators. The idle-source problem is real for telemetry: if a partition goes quiet, the watermark stalls and downstream windows never close. The configuration option table.exec.source.idle-timeout marks a source temporarily idle after a period without data; its default is 0, which disables idleness detection. Separately, table.exec.state.ttl removes a key’s state after a period without updates, with a default of 0 meaning state is never cleaned up. For a non-windowed GROUP BY device_id over a fleet where devices are retired, that default is a slow leak. The Flink concepts page is blunt about it: a running count per key means state grows as new keys appear, and if an expired key reappears it is treated as new and the count restarts at zero.
RisingWave offers the same watermark clause, and a useful extension. WATERMARK FOR column AS expr WITH TTL marks rows at or below the watermark as expired: late inserts, updates and deletes on them are ignored, and table state is cleaned by event time. Downstream materialized views keep results they already computed. By default, a RisingWave aggregation is emit-on-update, sending its current value at each barrier; EMIT ON WINDOW CLOSE holds a window back until the watermark passes its end, so the view never shows a partial count for the latest window. The documentation recommends that mode for append-only sinks and for aggregates such as percentiles that are costly to update incrementally.
In Materialize, lateness is a filter design decision. The documentation states that if a record’s timestamp falls outside a temporal filter’s window when it arrives, it is excluded, and the remedy is to widen the filter with a grace period.
All three therefore agree on the principle that event-time bounds are what let the engine forget. They disagree on whether you express that bound as a watermark (Flink, RisingWave) or a filter on the engine’s own clock (Materialize).

Figure 3: How a late reading is handled, from broker through watermark advance to window close and view update.
Figure 3 walks through a concrete case. A gateway reconnects and delivers a reading stamped 09:40:50 after the watermark has already passed 09:41:00. In a watermark engine the minute window ending 09:41:00 has closed, so the reading is dropped from that window (RisingWave documents exactly this for late events) or, with emit-on-update, may still adjust a view that has not yet closed the window. If late arrivals are common, you widen the watermark delay and accept more latency, or you route late events to a side channel for batch correction.
The same telemetry rollup in each engine
The scenario: a Kafka topic plant.telemetry carries JSON with device_id, site, metric, value and event_time. We want one-minute average, maximum and count of temperature per device, tolerating thirty seconds of lateness. The statements below follow the patterns in each vendor’s documentation, but we did not execute them against a live cluster for this article; validate syntax against the exact version you run.
RisingWave:
CREATE SOURCE telemetry (
device_id VARCHAR,
site VARCHAR,
metric VARCHAR,
value DOUBLE PRECISION,
event_time TIMESTAMP,
WATERMARK FOR event_time AS event_time - INTERVAL '30' SECOND
) WITH (
connector = 'kafka',
topic = 'plant.telemetry',
properties.bootstrap.server = 'broker:9092',
scan.startup.mode = 'earliest'
) FORMAT PLAIN ENCODE JSON;
CREATE MATERIALIZED VIEW temp_1m AS
SELECT device_id, window_start, window_end,
AVG(value) AS avg_c, MAX(value) AS max_c, COUNT(*) AS n
FROM TUMBLE(telemetry, event_time, INTERVAL '1' MINUTE)
WHERE metric = 'temperature_c'
GROUP BY device_id, window_start, window_end
EMIT ON WINDOW CLOSE;
Readers then run SELECT * FROM temp_1m WHERE device_id = 'pump-17' ORDER BY window_start DESC LIMIT 60 over a normal PostgreSQL connection. If you want live partial values for the current minute, drop the EMIT ON WINDOW CLOSE clause and accept emit-on-update behaviour.
Materialize:
CREATE CONNECTION kafka_conn TO KAFKA (BROKER 'broker:9092', SECURITY PROTOCOL = 'PLAINTEXT');
CREATE SOURCE telemetry_raw
FROM KAFKA CONNECTION kafka_conn (TOPIC 'plant.telemetry')
FORMAT JSON;
CREATE VIEW telemetry AS
SELECT data->>'device_id' AS device_id,
data->>'metric' AS metric,
(data->>'value')::float8 AS value,
(data->>'event_time')::timestamp AS event_time
FROM telemetry_raw;
CREATE VIEW telemetry_binned AS
SELECT device_id, value,
date_bin('1 minute', event_time, '2000-01-01 00:00:00+00')
+ INTERVAL '1 minute' AS window_end
FROM telemetry
WHERE metric = 'temperature_c'
AND mz_now() <= event_time + INTERVAL '7 days';
CREATE MATERIALIZED VIEW temp_1m AS
SELECT device_id, window_end,
avg(value) AS avg_c, max(value) AS max_c, count(*) AS n
FROM telemetry_binned
WHERE mz_now() >= window_end + INTERVAL '30 seconds'
AND mz_now() < window_end + INTERVAL '7 days'
GROUP BY device_id, window_end;
The + INTERVAL '30 seconds' on the emit filter is the grace period: each minute is published thirty seconds after it ends. Materialize’s temporal filter rules apply, notably that mz_now() clauses in materialized views may only be combined with AND.
Flink SQL:
CREATE TABLE telemetry (
device_id STRING,
site STRING,
metric STRING,
`value` DOUBLE,
event_time TIMESTAMP(3),
WATERMARK FOR event_time AS event_time - INTERVAL '30' SECOND
) WITH (
'connector' = 'kafka',
'topic' = 'plant.telemetry',
'properties.bootstrap.servers' = 'broker:9092',
'scan.startup.mode' = 'earliest-offset',
'format' = 'json'
);
CREATE TABLE temp_1m (
device_id STRING, window_start TIMESTAMP(3), window_end TIMESTAMP(3),
avg_c DOUBLE, max_c DOUBLE, n BIGINT,
PRIMARY KEY (device_id, window_start) NOT ENFORCED
) WITH (
'connector' = 'upsert-kafka',
'topic' = 'plant.temp_1m',
'properties.bootstrap.servers' = 'broker:9092',
'key.format' = 'json', 'value.format' = 'json'
);
SET 'table.exec.source.idle-timeout' = '30 s';
INSERT INTO temp_1m
SELECT device_id, window_start, window_end,
AVG(`value`), MAX(`value`), COUNT(*)
FROM TABLE(TUMBLE(TABLE telemetry, DESCRIPTOR(event_time), INTERVAL '1' MINUTES))
WHERE metric = 'temperature_c'
GROUP BY device_id, window_start, window_end;
Note what is missing: nothing in the Flink script lets a dashboard read temp_1m. The result lands in a topic. To serve it you add a consumer, a sink to a database, or a lakehouse table. That is not a flaw; it is the architecture, and it is exactly the cost you are weighing when you compare it with the other two.
Compare the three scripts by lines of operational surface, not lines of SQL. RisingWave and Materialize each end with a queryable object. Flink ends with a stream that you still have to serve.
Operations and Cost: What You Actually Pay For
Feature tables hide the part of the comparison that decides budgets: what the engine costs to run for a given amount of state and a given freshness target. None of the vendors publishes a comparable benchmark for IoT workloads, and cross-vendor numbers we have seen are produced by the vendor under test, so we do not quote any. What we can do is give you the cost model, with arithmetic you can redo with your own figures.
A worked sizing exercise (illustrative, not a benchmark)
Suppose 10,000 devices each report 5 metrics once per second. That is 50,000 readings per second, or about 4.3 billion per day. You want one-minute rollups per device per metric, and you keep the last 24 hours of rollups hot.
The number of open window keys at any instant is small: 10,000 devices times 5 metrics is 50,000 open aggregates, each holding a sum, a count, and a max. Even at a few hundred bytes of overhead per key, the working state is on the order of tens of megabytes. For a plain tumbling window, state is not your problem. Throughput is: 50,000 events per second means 50,000 state updates per second, and Flink’s MiniBatch option (table.exec.mini-batch.enabled, off by default, with allow-latency and size settings) exists precisely to buffer input and reduce state access for non-windowed aggregation.
Now change the query to a hopping window with a 10-minute size and 30-second hop. N is 20, so each reading lands in 20 windows, and the open keys become 50,000 times 20, or one million aggregates. Still modest, but the update rate is now one million state writes per second. This is the multiplier people forget, and RisingWave’s own documentation states the N-times row expansion for hop windows. If your dashboard only needs a rolling average, consider whether a cumulative or a tumbling window plus a query-time merge does the same job at one-twentieth of the write load.
The state that actually grows without bound is the unwindowed kind: “latest value per device,” “count per device since start,” and joins between the telemetry stream and a slowly changing asset table. A regular join must hold both inputs in state. For an asset table of a million rows joined to a reading stream, that is acceptable. For a join between two high-rate streams with no time bound, it is the classic way to run out of memory at 3 a.m. Flink’s interval joins and window joins use watermarks to keep state smaller, and its Flink 2.2 Delta Joins replace some large join state with lookups against source tables, with documented limits such as CDC sources without DELETE operations.
How each engine turns state into a bill
RisingWave puts durable state in object storage, which the project’s README describes as roughly 100 times cheaper than RAM; that is a vendor figure and depends on your cloud’s price list. Costs are then compute for streaming and serving nodes, object storage with request charges, and compactor CPU. Because state is not pinned to local disks, scaling compute does not require rebalancing data, and the README claims failure recovery in seconds; treat both as design properties to test, not as guarantees. The disk cache is the knob that trades money for read latency.
Materialize is priced and sized around cluster compute, because the working set lives in memory in arrangements. Memory is the dominant cost driver: the engine is happiest when the data it maintains fits in RAM, and temporal filters are the lever that keeps it so. The concepts documentation introduces the idea of reaction time, the combination of data freshness and query latency, as the quantity the system optimises. If you only need a view refreshed every few minutes, a memory-resident engine is a poor value; if you need sub-second freshness with strong consistency across views, it is the point of the product. Licensing is a cost too: self-managed use requires a license key under the BSL terms the docs describe.
Flink has the most knobs and the most operational surface. You run a JobManager and TaskManagers, or use a managed service from a cloud vendor; you size state backends; you plan savepoints. The Flink documentation warns that savepoints are reliably restorable only when both the query and the Flink version stay the same, with patch-level upgrades generally safe and major or minor upgrades possibly not. The 2.0 release also states that state compatibility between 1.x and 2.x is not guaranteed and removed several legacy APIs and configuration files. Budget a planned migration window if you are on 1.x. The upside is the ecosystem: more connectors, more window types, more production history, and a path to disaggregated state that removes the local-disk ceiling.
Operational questions to ask before you choose
How do you change a running query? In Flink, SQL job changes generally mean a new job and a state migration plan; the concepts page states savepoint restore is only reliable for unchanged queries. In RisingWave and Materialize, a new materialized view can be created alongside the old one and cut over, at the cost of a fresh backfill. Backfill time for a view over months of retained history is a real cost; Materialize calls the initial load of a source snapshotting and the state rebuild after restart hydration, and both take time proportional to the data involved.
How do you observe lag? In a barrier-based system, checkpoint duration and barrier latency are the vital signs. In Flink, checkpoint duration, backpressure, and watermark lag. In Materialize, reaction time. Choose the engine whose vital signs your on-call team can read.
How do you contain bad data? A single sensor emitting a timestamp in year 2099 will push a naive max-timestamp watermark far into the future and cause every normal event to be declared late. All three engines let you clamp or filter before the watermark, and you should: a defensive WHERE event_time < now() + INTERVAL '5' MINUTE in a cleaning view is cheap insurance.

Figure 4: Decision flow for choosing a streaming SQL engine, driven by serving needs, consistency needs, and ecosystem breadth.
Figure 4 encodes the choices in the order they usually bite. Need direct SQL serving to many readers? That removes plain Flink unless you add a serving store. Need strict cross-view consistency and accept memory-bound cost and BSL terms? Materialize. Need open-source, object-storage economics and a Postgres-compatible surface? RisingWave. Need broad connectors, complex event processing and the largest community? Flink.
Trade-offs, Gotchas, and What Goes Wrong
The watermark that never advances. Idle partitions are the most common cause of “my window never emits” in telemetry. A site goes offline for maintenance, one Kafka partition receives nothing, and the minimum watermark across partitions freezes. Flink gives you table.exec.source.idle-timeout (default disabled). With RisingWave, verify how your version treats idle partitions in the source; the documentation we read does not discuss it, so we cannot state the behaviour. Test it by pausing a producer.
Emit semantics surprise downstream consumers. Emit-on-update produces a stream of retractions and updates. An append-only sink or a naive consumer will see them as duplicates or corruption. RisingWave’s EMIT ON WINDOW CLOSE exists for this, at the cost of latency equal to your watermark delay. In Flink, a non-windowed aggregate produces a changelog and needs an upsert-capable sink; mismatches between source and sink changelog modes can silently add stateful operators such as ChangelogNormalize, a point the Flink concepts page makes explicitly.
State that never expires. Flink’s table.exec.state.ttl defaults to 0, meaning state is never cleaned up. A GROUP BY device_id job running for two years remembers every device it has ever seen. Setting a TTL is not free either: when a key expires and then returns, it restarts as new, so a lifetime counter silently resets. Decide per query whether you want forgetting or accuracy.
Retractions are not free in Materialize. The temporal filter documentation notes that the engine still has to maintain the upcoming retractions, and those consume resources. A seven-day filter over a high-rate stream holds seven days of data in the pipeline. Size the filter to the shortest horizon your questions need, and push long-horizon history to a lake.
Time zones and clocks. Flink’s table.local-time-zone converts TIMESTAMP WITH LOCAL TIME ZONE values using the session time zone, and a mismatch between a gateway’s local time and UTC produces windows that are off by hours in a way that looks like missing data. Normalise to UTC at ingest. This applies to every engine.
Exactly-once is a pipeline property. No engine can make an external HTTP call or a non-idempotent database write exactly-once by itself. If a rollup triggers a work order in a CMMS, make the receiver idempotent with a key derived from device and window.
Lock-in at the SQL surface. All three claim a SQL interface, but the dialects diverge on exactly the features IoT needs most: windows, time bounds, and emit control. Materialize’s mz_now() has no equivalent in the others; Flink’s DESCRIPTOR syntax is its own. A view library written for one engine is not portable, so plan for a translation layer or an abstraction at the metric-definition level.
Maturity and versions. RisingWave’s GitHub releases page listed v3.1.0 as the latest release, dated 21 Sep, with the page not showing the year; we therefore do not assert the date beyond that. Flink 2.2.0 was announced on December 4, 2025. We could not retrieve a current Materialize release number from the pages we read. Check the release notes for the version you plan to run.
Practical Recommendations
Start from the serving requirement, not the engine. If operators and APIs need to query rollups interactively with a standard driver, a PostgreSQL-compatible engine saves you a whole system. If the output is a feed into a lake, a downstream service or an ML pipeline, you do not need a built-in serving layer, and Flink’s connector breadth and window vocabulary dominate.
Pick RisingWave when you want open-source licensing, state that lives in object storage rather than in RAM, and Postgres compatibility for dashboards, with watermarks and emit-on-window-close for clean windowed output. It fits mid-to-large telemetry fleets where state is large, access is mostly recent, and cost per gigabyte of state matters.
Pick Materialize when correctness across several interdependent views is the product: for example, a twin whose “current state,” “alarm state” and “open work orders” views must agree at every read. Accept the BSL terms, plan for memory-bound sizing, and use temporal filters aggressively to bound state.
Pick Flink SQL when you already operate Flink, need the widest connector set, need complex event processing or ML inference in the stream, or need event-time semantics such as CUMULATE and SESSION windows with fine control. Plan the serving layer and the upgrade path explicitly, and look at Materialized Tables and ForSt as the direction of travel.
Do not rule out running two. A common and defensible pattern is Flink for heavy ingest, cleansing and enrichment into Kafka or Iceberg, and a Postgres-compatible streaming database for the low-latency views on top.
A short checklist before you commit:
- Write the five queries your operators run most often, and mark each as windowed, latest-state, or join.
- Estimate open keys per query, including the hop multiplier, and decide how each will forget old state.
- Define lateness in seconds per site, and test a paused producer and a backfilled batch.
- Decide the sink or serving contract, and whether consumers tolerate retractions.
- Prove recovery: kill a node under load and measure time to a fresh, correct view.
- Read the licence and the upgrade policy of the exact version you will run.
Frequently Asked Questions
What is streaming SQL?
Streaming SQL is the practice of expressing continuous computations over unbounded event streams using SQL. Instead of running a query once over stored data, you register it, and the engine keeps its result up to date as events arrive. In Flink the result is a dynamic table; in RisingWave and Materialize it is typically a materialized view that you can query directly. It lets analysts and engineers share one language for batch and stream logic.
What is incremental view maintenance?
Incremental view maintenance is the technique of updating a materialized view by computing only the change caused by new input, rather than recomputing the whole query. A new sensor reading adjusts one aggregate row instead of triggering a rescan. Materialize and RisingWave are designed around it, and Flink’s stateful operators and Materialized Tables apply the same principle. It keeps per-event cost roughly constant as history grows, provided state is bounded by windows or expiry.
Is RisingWave better than Flink for IoT?
Neither is better in general. RisingWave gives you an Apache 2.0 licensed engine, state in object storage, and a PostgreSQL-compatible query surface, so rollups are directly queryable. Flink offers a broader connector ecosystem, richer window and event-processing options, and years of production use, but needs a separate serving layer. Choose by serving needs, team skills, and whether you already run Flink, and test with your own telemetry before deciding.
Is Materialize open source?
Not in the OSI sense. Materialize’s documentation says Materialize Cloud and the Self-Managed Community edition are governed by the Business Source License, and that self-managed deployments need a license key. The repository describes the source as BSL 1.1 converting to Apache 2.0 after four years and states single-node use is free. Read the license file in the repository for the exact terms, including any change date, before building a product on it.
How do streaming SQL engines handle late sensor data?
They use event-time watermarks. You declare how far behind the maximum observed timestamp an event may be, such as 30 seconds, and the engine closes windows once the watermark passes their end. Events older than the watermark are late and are typically dropped from closed windows, as RisingWave’s documentation states. Materialize uses temporal filters instead and recommends a grace period. Choose the delay by measuring your actual gateway buffering behaviour.
Can these engines replace a time-series database?
Partly. They excel at continuously maintained rollups, alerts and latest-state views, and some can serve those views directly. They are not designed as long-retention stores for raw high-resolution history; state kept for bounded windows is the efficient mode. Most production designs keep raw data in a lake or time-series store and use the streaming SQL engine for the hot, derived layer that dashboards and automations query.
Further Reading
- Flink vs Spark Structured Streaming vs Kafka Streams in 2026 for the processor landscape this post builds on.
- Flink 2.2 ML_PREDICT and vector search for streaming AI for what the newest Flink release adds to SQL.
- Kafka vs Redpanda vs WarpStream for edge telemetry for choosing the broker that feeds these engines.
- NATS JetStream vs Kafka for edge IIoT telemetry for the lighter-weight edge option.
- RisingWave documentation and source repository for architecture, watermarks and emit semantics.
- Materialize documentation for concepts, isolation levels and temporal filters.
- Apache Flink documentation for window table-valued functions, state TTL and configuration.
By Riju — about
