Webinars

Building fast data loops from insert to query response in ClickHouse®

Recorded: November 26 @ 08:00 am PT
Presenters: Robert Hodges and Diego Nieto

This webinar explores what Altinity’s CEO, Robert Hodges, calls “fast data loops”: the end-to-end pattern of ingesting event data quickly, reacting to it in near real time, generating alerts or notifications, and managing the resulting state. The session is presented by Robert Hodges and Altinity support engineer Diego Nieto, with a focus on practical patterns drawn from security information and event management (SIEM) systems and similar observability use cases.

The session is organized around three core problems. First, getting data into ClickHouse® quickly, covering large batch inserts, async inserts with proper settings profile configuration, buffer tables and their trade-offs, and Kafka-based ingestion using the Kafka table engine with materialized view pipelines including dead-letter queue handling for parse errors. Second, generating alerts once the data is in ClickHouse, covering simple polling queries, materialized views backed by AggregatingMergeTree for faster outlier detection, materialized views that capture outliers directly into a dedicated table, the URL table engine as a webhook-style HTTP notification mechanism, and a full Kafka producer pipeline that reads from one topic, detects anomalies, and publishes alerts to another topic. Third, managing alert state using ReplacingMergeTree with versioning and an is_deleted column, where SELECT FINAL resolves the latest version at query time without expensive in-place mutations.

The session closes with a Q&A covering the performance characteristics of SELECT FINAL on ReplacingMergeTree, the importance of keeping version changes within the same partition, Kafka error handling mode, and the exactly-once semantics introduced in ClickHouse 24.8 via Keeper-managed offsets.

Here are the slides:

Key Moments (Timestamps)

Key moments generated with AI assistance.

  • 00:03 – Welcome and housekeeping
  • 01:32 – Speakers introductions: Robert Hodges and Diego Nieto
  • 02:30 – About Altinity: Altinity.Cloud, Altinity Kubernetes Operator for ClickHouse®, open-source projects
  • 03:24 – What is ClickHouse? Quick overview
  • 05:30 – What are fast data loops? SIEM use case overview
  • 07:21 – Topic 1: Inserting data quickly; large batch inserts
  • 11:08 – Small inserts from many clients: the problem
  • 12:44 – Async inserts: how they work, settings, and user profile setup
  • 17:02 – Async insert observability: system tables
  • 18:13 – Buffer tables: how they work and why to prefer async inserts
  • 22:34 – Kafka for durable ingestion: Kafka table engine and materialized view pipeline
  • 26:49 – Handling Kafka parse errors with the dead-letter queue pattern
  • 29:06 – Writing your own Kafka consumer for exactly-once semantics
  • 32:42 – Topic 2: Generating alerts; polling queries and their limitations
  • 33:50 – Materialized views with AggregatingMergeTree for faster outlier detection
  • 35:25 – Capturing outliers directly with a dedicated materialized view table
  • 37:13 – The URL table engine as an HTTP webhook notification mechanism
  • 39:52 – Full Kafka producer pipeline: detecting anomalies and publishing alerts
  • 43:18 – Topic 3: Managing alert state with ReplacingMergeTree
  • 49:55 – Q&A: ReplacingMergeTree performance, partition alignment, Kafka error handling, exactly-once semantics

Webinar Transcript

[00:03] – Welcome and Housekeeping

Robert: Hello everybody, and welcome to our webinar on building fast data loops from insert to query response in ClickHouse®. My name is Robert Hodges. I’m joined today by Diego Nieto as well as Kiara Teli.

Before I dive in, let me give you a little bit of housekeeping information. First, this meeting is being recorded, so you don’t have to frantically scribble notes. We will distribute an email with a link to the recording as well as the slides, so anything you see you can go ahead and get copies to cut and paste from. We do have the opportunity for questions here. This topic is somewhat experimental for us and there may be a little more give and take in this one. We’d love to hear your comments and questions about the problem we’re exploring. If you have questions along the way, type them into the Q&A box. Diego can answer them and we’ll also review them at the end of the call.


[01:32] – Speaker Introductions

Robert: I’ve been working on databases for 40 years, on Kubernetes since 2018, and on open-source databases since 2006. My day job is CEO of Altinity, but I’m basically a database geek.

Diego: Hello, my name is Diego. I’m a software engineer working in support, and I also like Robert deal with databases a lot: PostgreSQL, ClickHouse, and many others. I also do Python and R.

Robert: And mine are Go and Java. Great. A little bit about Altinity: we are an enterprise provider for ClickHouse databases. We operate a cloud called Altinity.Cloud. It runs in Amazon, GCP, Azure, and as of about a week ago, Hetzner, which is famous for low-cost computing. It’s very flexible. It can run in our account, or there’s also a BYOC option so it runs in your account. We have a bunch of customers using it both ways. We are also the authors of the Altinity Kubernetes Operator for ClickHouse®, which is the most popular way to run ClickHouse on Kubernetes. We also maintain Altinity Backup for ClickHouse®, the community Grafana provider, and a number of other open-source projects. We’re really big into open source.


[03:24] – What Is ClickHouse?

Robert: For those of you who don’t know it, ClickHouse is a real-time analytic database. The way I like to explain it is it’s kind of like a mix of a database like MySQL, which is open source, runs practically anywhere, and speaks SQL, and a traditional data warehouse like Vertica. That means it has a shared-nothing architecture with a bunch of nodes on a network with attached storage. It stores data in columns with high levels of compression. It can do parallel, vectorized execution, which means that as it’s processing the columns, it uses SIMD instructions to rip through the data as quickly as possible. It scales to many petabytes. We’re working on a system with 11 petabytes of compressed data, and there are systems larger than that on ClickHouse. It’s open source under the Apache 2.0 license. Because of all these properties, ClickHouse has become incredibly popular. It is the most popular engine for low-latency or real-time analytics.


[05:30] – What Are Fast Data Loops?

Robert: The goal in this presentation is to talk about fast data loops. This is a name I made up for this talk. The basic idea is that many systems using ClickHouse are trying to react quickly to external events. A classic example is SIEM, or security information and event management, where you have a system, often multi-tenant, scanning data from many different sources and looking for security problems: violations of policies, attempts to break into systems, or things that are just questionable, like somebody accessing S3 in an unexpected way.

These systems collect information and feed it into tables in the database. What we want to do is generate alerts or notifications that users can respond to and investigate further. This is a very common pattern across many use cases. We have ClickHouse in the center, and we’re going to talk about how to read data in quickly, how to generate alerts, and how to manage the state of an alert.


[07:21] – Topic 1: Inserting Data Quickly

Robert: The simplest way to put data into ClickHouse really quickly is big batches, and by big we mean really big. The default block size for ClickHouse is over a million rows, but you can extend that. There’s nothing wrong with putting 50 million rows in at a time if that’s how fast your data is arriving. All you need is enough memory. When those blocks come in, ClickHouse builds the parts it’s going to write to storage, sorts them, and flushes them to disk. If your application already does this, you may not need additional tricks.

The commands to do this are very simple. Here’s an example using a curl command to POST data through the HTTP interface. You basically pipe a CSV file directly into the curl command. Using this simple insert command you can insert 50 million rows if that’s what you want to do.

But not everybody has an application set up like this. There are a couple of cases where fast insert becomes a problem. The most common case is that in systems like SIEM, you actually have many sources of data: hundreds, thousands, or tens of thousands of machines generating relevant log information, all trying to log their data directly to ClickHouse. As a result, they’re going to have many small inserts coming from a lot of different locations. That’s the problem we’re going to focus on, because ClickHouse has a number of good ways to solve it.


[12:44] – Async Inserts: How They Work and How to Configure Them

Robert: The number one way to handle small inserts from many clients is async inserts. What an async insert does is automatically write the data into a buffer, and then, provided the client asks for it, hold the client connection and notify them when it actually commits to storage. You can have a bunch of clients executing insert commands simultaneously, and they’ll see their application pause briefly and then receive acknowledgement when the data is accepted, because it’s been collected together and pushed into the table in a big batch.

How do you enable async inserts? There are a number of properties you can set. You can set them on the insert statement itself by appending SETTINGS to the insert, but that’s kind of a painful way to do it because it means you have to change all your insert statements consistently. What is much simpler is to create a user with a settings profile. What we’re doing here is creating an async profile with the required settings. The first one turns async inserts on. The second says to wait for the data to be processed so the application will know if it fails. The third sets how long to wait, for example 10 seconds. And then there’s a fourth important one: as of ClickHouse 24.3, there’s a setting called async_insert_use_adaptive_busy_timeout. In 24.3, ClickHouse introduced adaptive async insert where it figures out based on your data what a reasonable level of buffering is, but if you fail to turn that off, it will ignore your other settings. So if you want to set these explicitly and not use adaptive async insert, you need to turn that setting off.

Once you create this profile and assign it to a user, that user’s inserts will automatically be buffered and merged. It’s as simple as that. For those who tried async inserts back in 2023 and found them somewhat unstable, that’s no longer the case. A lot of the issues have been fixed. This is a very capable feature.

Diego: And it has a lot of observability with tables you can query in the query log and the async insert log.

Robert: Exactly. Async inserts have great observability via two system tables. While an insert is waiting, it appears in the system.asynchronous_inserts table. Once it has been inserted into the database, it goes into the log. You want to ensure that log table is enabled and then build your observability around it so you can track how many pending async inserts you have at any given time and how many you have over time. Like all ClickHouse system tables, they’re very accessible and easy to understand.


[18:13] – Buffer Tables: How They Work and When Not to Use Them

Robert: Let’s talk about an older way to handle small inserts: buffer tables. We don’t recommend them now that async inserts have gotten so good, but people still know about them and they can sometimes be useful.

A buffer table sits in front of the table you’re actually updating and does exactly what the name implies: it buffers inserts in memory. You create it using the Buffer engine, reference the target table so you don’t have to repeat the column definitions, and then set a bunch of parameters: how many independent buffers to use for parallelism, minimum and maximum hold times in seconds, and minimum and maximum row counts before flushing.

When you insert into the buffer table, the data appears in memory. You can do selects on the buffer table and it will show you all the data, including both what it has buffered and the underlying target table data.

There are downsides. Buffer tables have some corner cases, particularly around sorting.

Diego: The biggest problem is durability. If your database fails or has some other problem, this is an in-memory table and your data will be lost with no notification to the application. It’s like having wait_for_async_insert = 0.

Robert: Exactly. So if you really don’t care whether the data gets in or not, this could be fine. But the async inserts have a key advantage: your applications are held and they’ll see the connection fail and can retry. But to be clear, much of the data that flows into ClickHouse for monitoring, for example, consists of monotonically increasing counters or measurements where losing two seconds doesn’t matter much. Now if you lost a few hours, that would be a problem, but brief losses often aren’t a big deal.


[22:34] – Kafka for Durable Ingestion

Robert: If you do care about the data, the answer in a word is Kafka. Kafka is a distributed log where producers push data into topics and consumers read from it. If the producers generating temperature readings push the data into Kafka, it gets buffered up in durable records that ClickHouse can then read in big chunks. This helps solve the small insert problem.

One of the simplest and fastest ways to do this is using the Kafka table engine. This is a built-in table engine that takes a Kafka topic and wraps it so it looks like a SQL table. When you do a select from it you’re effectively reading from the topic. The next thing you do is set up a materialized view. Materialized views are like triggers that automatically fire whenever data is inserted into the table they’re based on. When the insert occurs, the materialized view causes a query to run automatically on that data, and the results get put in a target MergeTree table.

So the chain looks like this: something arrives in Kafka, the Kafka table engine reads it, the materialized view fires, and the result gets stored in MergeTree.

Here’s an example of setting up a Kafka table. You use ENGINE = Kafka and the settings give the topic list, broker addresses, consumer group name, and most importantly the data format, in this case CSV with column headings.

Diego: The Kafka format is important and ClickHouse will use its inference mechanism to get the types. And with settings like input_format_defaults_for_omitted_fields, it will write defaults instead of nulls for missing data and do a lot of stuff in the background.

Robert: The treatment of formats has gotten really good over the last few years. For example, if you’re reading Parquet, it can map columns to nullable types. These things have really improved.


[26:49] – Handling Kafka Parse Errors: The Dead-Letter Queue Pattern

Robert: One problem with this Kafka pipeline is that if the conversion from CSV fails, you get an error that pops up in the log and ClickHouse just keeps trying to re-read the message. What we really want to do is let stuff fail gracefully and put it in another queue.

There is a way to do that. You can create a Kafka error handling materialized view that specifically looks for Kafka parse errors. By setting kafka_handle_error_mode = 'stream', Kafka will write the error and the raw message to two new virtual columns: _error and _raw_message. You create a materialized view that selects where the error message is non-empty and puts those rows into a dead-letter table. You can then inspect that table to find out what’s going wrong.

When you put this all together: something comes in, it’s written to the Kafka topic, the Kafka table engine reads it, if it’s successful it goes through the main materialized view into MergeTree, and if it fails it goes into the errors table. This is pretty flexible.

Diego: This is mainly for parsing errors, like malformed messages in the Kafka topic. Without this approach, you wouldn’t see those messages in the main table and you’d be asking yourself where they went. They’re not going to be ingested because parsing failed. You could check the Kafka debug log, but that’s very verbose. This materialized view approach is much more convenient. In recent versions, ClickHouse’s inference mechanism has gotten much better at dealing with type mismatches automatically, like substituting nulls for zeros or empty strings. But for truly malformed JSON with missing brackets, those will still land in the dead-letter queue.

Robert: That’s a total lifesaver. Binary formats like Protobuf can generate stuff that you simply can’t read, and without trapping the data you’re trying to read, it’s really hard to debug what’s going on.


[29:06] – Writing Your Own Kafka Consumer

Robert: What if the Kafka table engine isn’t good enough? One case is that the Kafka table engine’s default semantics are at-least-once, so you can get duplicates. This can happen when Kafka rebalances topics, which causes different consumers to re-read stuff. If you want to avoid those problems, you can write your own application using a library like librdkafka. You build a consumer in your favorite language, virtually every common language is supported, and you use the transactional semantics of the library to read from Kafka and apply to the target table with exactly-once guarantees.

Diego: Since ClickHouse 24.8, you can actually get exactly-once semantics using the Kafka engine itself. You use Keeper to store and manage the offsets, so ClickHouse tracks exactly what it has read. It may still be experimental, but it’s definitely something to look at.

Robert: Good to know. The big problem with exactly-once is just remembering what you read, so that if a topic gets rebalanced or you crash, you have the right restart point. The ClickHouse Kafka Connect project from ClickHouse Incorporated is another option: an external connector that reads from Kafka and puts it into ClickHouse.

Diego: For me, Kafka Connect just adds complexity. Writing your own consumer is the best way to go for exactly-once semantics.


[32:42] – Topic 2: Generating Alerts

Robert: There are a number of ways to generate alerts outside of ClickHouse, for example using Fluentd or Fluent Bit to process events coming out of Kafka and notify people. But if you’re just building an application to generate alerts, there’s a simple answer: poll for them. This is a terrible pattern on large data warehouses but if you have a small number of expected results, you could just select any temperature reading above 90. However, as soon as you get larger data sets, that select is likely to be expensive.

One of the things you might want to do to speed up polling is use a materialized view. Here’s an example that looks for Min and Max temperatures per hour by creating a table using AggregatingMergeTree. SimpleAggregateFunction is a compact way to store the max or min value you’ve seen. You partition by hour, which is your aggregation key, and the materialized view fires whenever something arrives in the source table and pushes the results into the outliers table. This table is a lot faster to scan than looking at the actual raw data.


[35:25] – Capturing Outliers with a Dedicated Materialized View Table

Robert: However, we can do one better. We can remember outliers somewhere so we don’t actually have to poll for them at all. Here’s an example: we create a table called test_outliers_2, partitioned by month so it captures all the outliers per month, and the materialized view for this is very simple. Any temperature greater than 90 gets stuck in this table. You can then see all of those outliers very very quickly without scanning the main data set. If you do this regularly, it’s much simpler and you scan a lot less data.


[37:13] – The URL Table Engine as an HTTP Webhook

Robert: We can do one better still: we can actually tell somebody about it actively. For that we can use the URL engine, which allows you to do HTTP GETs and POSTs to any web endpoint. You create a table that has your data types, set the engine to URL, give it the endpoint where you want to send the data, and specify the format such as CSV. Then you create a materialized view that fires whenever a temperature greater than 90 arrives in the test table and sends a CSV record to that URL.

What this looks like in practice is: data is inserted, the materialized view fires, and it sends an HTTP POST with chunked transfer encoding. Chunked transfer encoding is important here: instead of receiving the data as a single blob, it’s split up into chunks sent in sequence. This is an optimization on the HTTP protocol, and any standard web server supports it by default. When you set up a simple server to receive these, every time the materialized view fires you’ll see a message in your log. This is a good webhook-style approach for getting alerts, and it can be connected to an application that puts them somewhere else or triggers further action.


[39:52] – Full Kafka Producer Pipeline: Detecting Anomalies and Publishing Alerts

Robert: We can also use Kafka to publish alerts out. Here’s an example of reading data from Kafka, looking for outliers, and posting them back out to a different Kafka topic.

The architecture is: a Kafka consumer table reads temperature data from a topic. A materialized view reads from it and writes any anomalies worth alerting on to an intermediate temp_alerts table. A Kafka producer table is a Kafka table configured to write to an alert topic. And a Kafka producer materialized view fires whenever something shows up in temp_alerts and selects that data into the Kafka producer table, which sends the messages.

Here’s the code. The consumer is just like the previous example. The materialized view now looks for conditions that should trigger an alert. The intermediate table stores time, temperature, and alert level. The Kafka producer table configures the output topic. And finally the materialized view that feeds the producer includes a multiIf expression that assigns alert levels based on severity ranges, so you can encode logic like “this boiler is about to blow up” directly into the alert payload.

This is a really nice pattern because you can add any kind of logic you want in that query to reshape the data and generate the best possible alert. In this case we’re not really using ClickHouse to store much data long-term. The alerts are a small fraction of the data, and Kafka is acting as a pipeline to process things coming in and publish things going out.


[43:18] – Topic 3: Managing Alert State with ReplacingMergeTree

Robert: The final topic is how to manage the state relating to alerts. One of the most common approaches is ReplacingMergeTree. This is a table type that allows you to change data by doing inserts, which is always a good thing when you have very large data sets, because actually updating rows using ALTER TABLE UPDATE or lightweight deletes can be very expensive in large tables, particularly for observability data sets.

Here’s the example. We take our previous table definition and add a few new fields: an alert_id that we generate externally, an acknowledged flag (0 for unacknowledged, 1 for acknowledged), an is_deleted flag, and an effective_date that tracks when the row was changed.

ReplacingMergeTree takes a versioning column. Here effective_date serves as the version: as it increases, later values take precedence. The is_deleted column is a flag that says whether this row should be treated as deleted. The ORDER BY clause is critical: it defines what counts as the same row. Anything with the same alert_id is treated as the same row, and the latest version wins.

How does this work in practice? When you first insert an alert, version 0 is the first entry. Later, if you want to acknowledge it, you insert a new row with the same alert_id, acknowledged = 1, and a later effective_date. When you want to delete it, you insert another row with is_deleted = 1.

How do you get it to disappear? That gets resolved at read time. You use SELECT ... SETTINGS final = 1, which tells ClickHouse to apply the deduplication logic at query time so only the latest version of each alert_id is returned. If the latest version has is_deleted = 1, the row disappears.


[49:55] – Q&A

Viewer question: Does using ReplacingMergeTree degrade read query performance a lot compared to regular MergeTree?

Diego: No. The only performance issue you have with ReplacingMergeTree is when you want to deduplicate, which is exactly what this engine is for. You use the FINAL clause in the select, so the data will be deduplicated in memory. That’s where the performance hit comes in, but not in reading the raw data itself.

Robert: The really important thing though is when you’re doing deduplication, the changes need to land in the same partition. If they land in different partitions, that’s very expensive because you actually have to read a potentially very large portion of the table to decide whether something needs to be deduplicated. In this design, because we only change the acknowledged and is_deleted fields and keep the alert time the same, changes should always land in the same partition and will resolve quickly. If you don’t do that, it could be very slow.

Robert: There was a question about the Kafka error handling mode and how it works. Diego, do you want to cover that?

Diego: Sure. The kafka_handle_error_mode = 'stream' setting is mainly for parsing errors. If you have malformed messages in the Kafka topic, without this approach you wouldn’t see those messages in the main table. They won’t be ingested because parsing failed. You’d be asking yourself where they went. With this setting enabled, Kafka populates virtual columns with the error and the raw message, and you can create a materialized view that collects those into a dead-letter table. In recent ClickHouse versions, the inference mechanism handles a lot of type mismatches automatically, like substituting nulls or defaults. But for truly malformed JSON, those will still appear in the dead-letter queue.

Diego: On exactly-once semantics: since ClickHouse 24.8, you can get exactly-once semantics using the Kafka engine with Keeper managing the offsets. There’s a KB article we can share for that. The classic approach of writing your own Kafka consumer with transactional library semantics is still valid, but it’s now possible to achieve this within the Kafka engine itself.


FAQ Section

Q: What is the best way to handle many small inserts from different clients into ClickHouse?

A: The recommended approach is async inserts. When enabled, ClickHouse buffers incoming inserts in memory and flushes them to storage as a single larger block once a time or size threshold is reached. The easiest way to enable them is to create a dedicated ClickHouse user with a settings profile that includes async_insert = 1, wait_for_async_insert = 1, a flush timeout, and async_insert_use_adaptive_busy_timeout = 0 if you want deterministic buffering behavior. Any client that inserts using this user will automatically benefit from batching. As of ClickHouse 24.3, if async_insert_use_adaptive_busy_timeout is left at its default of 1, it will override manually configured timeout settings. For applications where brief data loss on crash is acceptable, setting wait_for_async_insert = 0 gives higher throughput, but clients will not know if an insert failed.

Q: When should I use Kafka in front of ClickHouse instead of async inserts?

A: Kafka is the right choice when you need durable, guaranteed delivery and cannot afford to lose data even if ClickHouse restarts. Kafka acts as a persistent distributed log, so data is safely stored in Kafka until ClickHouse successfully consumes it. Async inserts buffer data in ClickHouse memory and will lose buffered rows on a crash if wait_for_async_insert = 0. Kafka also decouples producers from ClickHouse so thousands of clients can write at their own rate without directly impacting ClickHouse insert throughput. For monitoring or observability data where losing a few seconds is acceptable, async inserts are simpler and sufficient. For financial, security, or compliance data, Kafka provides the durability guarantees you need.

Q: How does the Kafka table engine handle parse errors?

A: By default, if a message in a Kafka topic cannot be parsed, ClickHouse logs an error and skips the message. The recommended pattern for capturing failed messages is to enable Kafka error handling mode by setting kafka_handle_error_mode = 'stream'. With this setting, ClickHouse populates two virtual columns, _error and _raw_message, for rows that failed to parse. You then create a separate materialized view that selects rows where _error is non-empty and writes them to a dead-letter MergeTree table. This allows you to inspect malformed messages directly with SQL rather than digging through verbose Kafka debug logs.

Q: What is ReplacingMergeTree and when should it be used for alert state management?

A: ReplacingMergeTree is a MergeTree variant that handles deduplication and updates by treating inserts with the same ORDER BY key as new versions of the same row, keeping only the latest version at merge or query time. It is well suited for managing alert state because it avoids the expense of ALTER TABLE UPDATE or lightweight deletes on large tables. To use it for alerts, define a versioning column such as effective_date and an is_deleted column. When an alert is acknowledged or deleted, insert a new row with the same alert_id, updated fields, and a newer version value. Query the table using SETTINGS final = 1 to get the latest state of each alert, with deleted rows automatically excluded. The critical design rule is that all version changes for the same row must land in the same partition; otherwise, SELECT FINAL must read across multiple partitions, which is significantly more expensive.

Q: How can I speed up outlier detection without scanning the entire raw data table?

A: Use a materialized view backed by an AggregatingMergeTree table to pre-compute the metrics you care about. For example, to detect temperature outliers, create an AggregatingMergeTree table that stores Min and Max temperatures per hour using SimpleAggregateFunction. A materialized view fires on every insert into the source table and updates this aggregated table automatically. Scanning the aggregated table is many times faster than scanning the raw data. For even more direct access to outliers, create a second materialized view that filters the source table directly for rows exceeding your threshold and writes them to a plain MergeTree outlier table. This table will be very small and nearly instant to query, since it only ever receives rows that already exceed the alert condition.

Q: What is the URL table engine, and when is it useful for alerting?

A: The URL table engine lets you insert data to or read data from any HTTP endpoint directly from a ClickHouse table definition. For alerting, you define a URL table pointing to a webhook endpoint, then create a materialized view that fires when a threshold condition is met and selects the matching rows into the URL table. ClickHouse will send an HTTP POST with the data in your chosen format, such as CSV. The receiving server needs to support chunked transfer encoding, which all standard web frameworks handle by default. This is a simple, low-infrastructure way to push real-time notifications to external systems, ticketing tools, or custom applications without needing additional message queue infrastructure.


© Altinity, Inc. All rights reserved. Altinity®, Altinity.Cloud®, and Altinity Stable® are registered trademarks of Altinity, Inc. ClickHouse® is a registered trademark of ClickHouse, Inc.; Altinity is not affiliated with or associated with ClickHouse, Inc. Kubernetes, MySQL, and PostgreSQL are trademarks and property of their respective owners.

Join our Slack

ClickHouse® is a registered trademark of ClickHouse, Inc.; Altinity is not affiliated with or associated with ClickHouse, Inc.

Related:

Leave a Reply

Your email address will not be published. Required fields are marked *