ClickHouse storage
ClickHouse is a columnar database designed for analytical workloads. The @mastra/clickhouse package provides storage adapters for several Mastra storage domains and is the recommended backend for production observability.
ClickHouse is most commonly used as the dedicated observability backend in a composite storage setup, with another database serving the remaining domains.
When to use ClickHouseDirect link to When to use ClickHouse
Production observability for traces, logs, metrics, scores, and feedback.
For local development, use a composite store that combines LibSQL (for memory and workflows) and @mastra/duckdb (for observability). Neither alone covers a development setup: LibSQL doesn't implement the observability domain, and DuckDB doesn't implement the other domains. See the observability overview for an example.
InstallationDirect link to Installation
- npm
- pnpm
- Yarn
- Bun
npm install @mastra/clickhouse@latest
pnpm add @mastra/clickhouse@latest
yarn add @mastra/clickhouse@latest
bun add @mastra/clickhouse@latest
You will also need a running ClickHouse server. See Hosting options for managed and self-hosted choices.
UsageDirect link to Usage
Observability with vNext (recommended)Direct link to Observability with vNext (recommended)
ObservabilityStorageClickhouseVNext is the current observability domain implementation. It uses an insert-only schema backed by ReplacingMergeTree and is optimized for the volume produced by traces, logs, metrics, scores, and feedback.
Compose it with another storage adapter so observability writes don't contend with your application data:
import { Mastra } from '@mastra/core'
import { MastraCompositeStore } from '@mastra/core/storage'
import { PostgresStore } from '@mastra/pg'
import { ObservabilityStorageClickhouseVNext } from '@mastra/clickhouse'
import { Observability, MastraStorageExporter } from '@mastra/observability'
const observabilityStore = new ObservabilityStorageClickhouseVNext({
url: process.env.CLICKHOUSE_URL!,
username: process.env.CLICKHOUSE_USERNAME!,
password: process.env.CLICKHOUSE_PASSWORD!,
})
export const mastra = new Mastra({
storage: new MastraCompositeStore({
id: 'composite-storage',
default: new PostgresStore({
id: 'pg',
connectionString: process.env.DATABASE_URL!,
}),
domains: {
observability: observabilityStore,
},
}),
observability: new Observability({
configs: {
default: {
serviceName: 'mastra',
exporters: [new MastraStorageExporter()],
},
},
}),
})
MastraStorageExporter automatically selects the insert-only strategy when ClickHouse is the observability backend, which gives the highest write throughput. See tracing strategies for details.
Observability with the legacy domainDirect link to Observability with the legacy domain
ObservabilityStorageClickhouse is the original observability adapter and remains supported for projects that haven't migrated to the vNext schema. The configuration shape is the same as the vNext class.
import { ObservabilityStorageClickhouse } from '@mastra/clickhouse'
const observabilityStore = new ObservabilityStorageClickhouse({
url: process.env.CLICKHOUSE_URL!,
username: process.env.CLICKHOUSE_USERNAME!,
password: process.env.CLICKHOUSE_PASSWORD!,
})
New projects should use ObservabilityStorageClickhouseVNext instead.
ClickHouse for every domainDirect link to ClickHouse for every domain
ClickhouseStoreVNext backs the memory, workflows, and observability domains with ClickHouse and uses the vNext observability adapter automatically. Use it when you want ClickHouse to back the entire application without wiring a composite store manually.
import { Mastra } from '@mastra/core'
import { ClickhouseStoreVNext } from '@mastra/clickhouse'
export const mastra = new Mastra({
storage: new ClickhouseStoreVNext({
id: 'clickhouse-storage',
url: process.env.CLICKHOUSE_URL!,
username: process.env.CLICKHOUSE_USERNAME!,
password: process.env.CLICKHOUSE_PASSWORD!,
}),
})
ClickhouseStoreVNext accepts the same configuration as ClickhouseStore and reuses the same ClickHouse client across every domain.
Manual compositionDirect link to Manual composition
ClickhouseStore is the long-standing class that backs every domain with the legacy observability adapter. New projects should prefer ClickhouseStoreVNext. If you need to customize the composite (for example, to override one domain with a different backend), build it manually:
import { Mastra } from '@mastra/core'
import { MastraCompositeStore } from '@mastra/core/storage'
import { ClickhouseStore, ObservabilityStorageClickhouseVNext } from '@mastra/clickhouse'
const credentials = {
url: process.env.CLICKHOUSE_URL!,
username: process.env.CLICKHOUSE_USERNAME!,
password: process.env.CLICKHOUSE_PASSWORD!,
}
export const mastra = new Mastra({
storage: new MastraCompositeStore({
id: 'composite-storage',
default: new ClickhouseStore({ id: 'clickhouse-storage', ...credentials }),
domains: {
observability: new ObservabilityStorageClickhouseVNext(credentials),
},
}),
})
Bring your own ClickHouse clientDirect link to Bring your own ClickHouse client
Pass a pre-configured client when you need custom connection settings such as request timeouts, compression, or interceptors:
import { createClient } from '@clickhouse/client'
import { ClickhouseStore } from '@mastra/clickhouse'
const client = createClient({
url: process.env.CLICKHOUSE_URL!,
username: process.env.CLICKHOUSE_USERNAME!,
password: process.env.CLICKHOUSE_PASSWORD!,
request_timeout: 60_000,
compression: { request: true, response: true },
})
const storage = new ClickhouseStore({ id: 'clickhouse-storage', client })
The same client form is accepted by ObservabilityStorageClickhouse and ObservabilityStorageClickhouseVNext.
ConfigurationDirect link to Configuration
ClickhouseStore optionsDirect link to clickhousestore-options
id:
url?:
username?:
password?:
client?:
ttl?:
replication?:
disableInit?:
ClickhouseStore also accepts every option from ClickHouseClientConfigOptions (such as database, request_timeout, compression, keep_alive, and max_open_connections).
Replicated clustersDirect link to Replicated clusters
Use replication when Mastra writes to a multi-replica ClickHouse cluster through a load balancer.
const storage = new ClickhouseStoreVNext({
id: 'clickhouse-storage',
url: process.env.CLICKHOUSE_URL!,
username: process.env.CLICKHOUSE_USERNAME!,
password: process.env.CLICKHOUSE_PASSWORD!,
replication: {
cluster: 'company_cluster',
},
})
When replication is set, Mastra rewrites its MergeTree and ReplacingMergeTree table engines to ReplicatedMergeTree and ReplicatedReplacingMergeTree. The default engine arguments are:
zookeeperPath:'/clickhouse/tables/{shard}/{database}/{table}'replicaName:'{replica}'
The defaults match the most common self-managed convention. If your cluster's existing tables use a different layout (for example /clickhouse/tables/{shard}/{table} without the {database} segment), set zookeeperPath explicitly to match. Mastra does not read your cluster's convention from Keeper, so a mismatched default writes Mastra's metadata to a separate branch from the rest of the cluster.
new ClickhouseStoreVNext({
url: process.env.CLICKHOUSE_URL!,
username: process.env.CLICKHOUSE_USERNAME!,
password: process.env.CLICKHOUSE_PASSWORD!,
replication: {
cluster: 'company_cluster',
zookeeperPath: '/clickhouse/tables/{shard}/{table}',
},
})
Set cluster to add ON CLUSTER to Mastra-owned DDL such as table creation, materialized view creation, column migrations, TTL changes, and table drops.
Manual maintenance such as optimizeTable() and materializeTtl() runs on every replica when cluster is set. These operations can be expensive on a large cluster. Prefer running them outside peak hours, and let routine merges happen on the background merge queue rather than triggering them on every restart.
If existing Mastra tables use local MergeTree or ReplacingMergeTree engines, initialization fails while replication is enabled. Mastra refuses to silently convert local tables because copy-and-swap is unsafe across replicas. To migrate, recreate the affected tables as Replicated* before enabling replication. The typical sequence is: rename the local table, run CREATE TABLE ... ENGINE = ReplicatedMergeTree(...) ON CLUSTER ..., run INSERT INTO ... SELECT * FROM <renamed_local>, then drop the renamed local table.
Do not set replication on ClickHouse Cloud. Cloud rewrites MergeTree to SharedMergeTree server-side, and explicit ReplicatedMergeTree engines produce incorrect DDL. replication is only for self-managed multi-replica clusters.
Observability domain optionsDirect link to Observability domain options
ObservabilityStorageClickhouse and ObservabilityStorageClickhouseVNext accept the same connection options as ClickhouseStore (url, username, password, or a pre-configured client).
Hosting optionsDirect link to Hosting options
ClickHouse runs anywhere you can reach it over HTTP. Two common choices:
- ClickHouse Cloud: Managed service with a free trial tier. Provides connection details directly compatible with
url,username, andpassword. - Self-hosted: Run the official
clickhouse/clickhouse-servercontainer or install from the official packages. Suitable for VPS, dedicated hardware, or Kubernetes.
For local development:
docker run -d --name mastra-clickhouse \
-p 8123:8123 -p 9000:9000 \
-e CLICKHOUSE_USER=default \
-e CLICKHOUSE_PASSWORD=password \
clickhouse/clickhouse-server
new ObservabilityStorageClickhouseVNext({
url: 'http://localhost:8123',
username: 'default',
password: 'password',
})
Deploying with Railway and similar platformsDirect link to Deploying with Railway and similar platforms
Platforms like Railway, Fly.io, Render, and Heroku run application containers on ephemeral filesystems. Embedded observability backends such as DuckDB require a writable, persistent local file, so they either lose data on restart or fail to deploy entirely on these platforms.
Use ClickHouse instead. Because ClickHouse is reached over HTTP, the same connection works from any host:
import { Mastra } from '@mastra/core'
import { MastraCompositeStore } from '@mastra/core/storage'
import { PostgresStore } from '@mastra/pg'
import { ObservabilityStorageClickhouseVNext } from '@mastra/clickhouse'
import { Observability, MastraStorageExporter } from '@mastra/observability'
export const mastra = new Mastra({
storage: new MastraCompositeStore({
id: 'composite-storage',
default: new PostgresStore({
id: 'pg',
connectionString: process.env.DATABASE_URL!,
}),
domains: {
observability: new ObservabilityStorageClickhouseVNext({
url: process.env.CLICKHOUSE_URL!,
username: process.env.CLICKHOUSE_USERNAME!,
password: process.env.CLICKHOUSE_PASSWORD!,
}),
},
}),
observability: new Observability({
configs: {
default: {
serviceName: 'mastra',
exporters: [new MastraStorageExporter()],
},
},
}),
})
Two ways to provision the database:
- Managed: Use ClickHouse Cloud. Set
CLICKHOUSE_URL,CLICKHOUSE_USERNAME, andCLICKHOUSE_PASSWORDas environment variables in your hosting platform. - Self-hosted on Railway: Add a ClickHouse service to your Railway project from the official Docker image, then reference it in the application service through Railway's private networking.
The same approach applies to other hosts with ephemeral filesystems. For application data that should also live off-host, pair this setup with a managed PostgreSQL or LibSQL/Turso instance for the default storage.
Don't point an embedded backend like DuckDB at a path inside an ephemeral container filesystem. Data written there is lost when the container restarts, and on some platforms the path is read-only.
InitializationDirect link to Initialization
When passed to the Mastra class, ClickhouseStore calls init() automatically to create the schema and run any pending migrations. The same applies to ObservabilityStorageClickhouseVNext when used through MastraCompositeStore.
If you manage storage outside of Mastra, call init() explicitly:
import { ObservabilityStorageClickhouseVNext } from '@mastra/clickhouse'
const observability = new ObservabilityStorageClickhouseVNext({
url: process.env.CLICKHOUSE_URL!,
username: process.env.CLICKHOUSE_USERNAME!,
password: process.env.CLICKHOUSE_PASSWORD!,
})
await observability.init()
In CI/CD pipelines, set disableInit: true on ClickhouseStore and run init() from a deployment step that uses elevated credentials. Runtime application credentials can then be limited to read and insert.
ObservabilityDirect link to Observability
ClickHouse is the recommended backend for production observability:
- Insert-only strategy:
MastraStorageExporterwrites completed spans in batches without per-span updates, which is the highest-throughput strategy available. - Columnar compression: Span attributes and log payloads compress well compared to the same data in row-oriented databases.
For the full strategy matrix and production guidance, see the MastraStorageExporter reference.