Skip to main content

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. It can also back the supported domains on its own through ClickhouseStore.

When to use ClickHouse
Direct link to When to use ClickHouse

  • Production observability for traces, logs, metrics, scores, and feedback.
  • Append-heavy workloads where columnar storage and compression help keep costs down.
  • Deployment platforms with ephemeral filesystems (such as Railway, Fly.io, Render, Heroku, or container schedulers) where embedded backends like DuckDB cannot persist data.

For local development, LibSQL or @mastra/duckdb are usually a better fit because they need no external service.

Installation
Direct link to Installation

npm install @mastra/clickhouse@latest

You will also need a running ClickHouse server. See Hosting options for managed and self-hosted choices.

Usage
Direct link to Usage

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 do not contend with your application data:

src/mastra/index.ts
import { Mastra } from '@mastra/core'
import { MastraCompositeStore } from '@mastra/core/storage'
import { LibSQLStore } from '@mastra/libsql'
import { ObservabilityStorageClickhouseVNext } from '@mastra/clickhouse'
import { Observability, DefaultExporter } 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 LibSQLStore({
id: 'mastra-storage',
url: 'file:./mastra.db',
}),
domains: {
observability: observabilityStore,
},
}),
observability: new Observability({
configs: {
default: {
serviceName: 'mastra',
exporters: [new DefaultExporter()],
},
},
}),
})

DefaultExporter 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 domain
Direct link to Observability with the legacy domain

ObservabilityStorageClickhouse is the original observability adapter and remains supported for projects that have not 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.

Full storage with ClickhouseStore
Direct link to full-storage-with-clickhousestore

ClickhouseStore implements the memory, workflows, scores, and observability domains. Use it when you want a single backend for the supported domains. For most observability deployments, the composite setup above is preferable because it isolates observability writes from primary application data.

src/mastra/index.ts
import { Mastra } from '@mastra/core'
import { ClickhouseStore } from '@mastra/clickhouse'

export const mastra = new Mastra({
storage: new ClickhouseStore({
id: 'clickhouse-storage',
url: process.env.CLICKHOUSE_URL!,
username: process.env.CLICKHOUSE_USERNAME!,
password: process.env.CLICKHOUSE_PASSWORD!,
}),
})

Bring your own ClickHouse client
Direct 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.

Configuration
Direct link to Configuration

ClickhouseStore options
Direct link to clickhousestore-options

id:

string
Unique identifier for this storage instance.

url?:

string
ClickHouse server URL (for example, `https://your-instance.clickhouse.cloud:8443` or `http://localhost:8123`). Required when not passing a pre-configured `client`.

username?:

string
ClickHouse username. Required when not passing a pre-configured `client`.

password?:

string
ClickHouse password. Required when not passing a pre-configured `client`. Can be an empty string for the default user on a local instance.

client?:

ClickHouseClient
Pre-configured ClickHouse client from `@clickhouse/client`. Use this when you need custom request settings. Mutually exclusive with the credential fields above.

ttl?:

object
Per-table TTL configuration applied at table creation time. Accepts row-level and column-level TTLs in interval units from `NANOSECOND` through `YEAR`.

disableInit?:

boolean
= false
When `true`, the store does not run table creation or migrations on first use. Call `storage.init()` explicitly from your deployment scripts.

ClickhouseStore also accepts every option from ClickHouseClientConfigOptions (such as database, request_timeout, compression, keep_alive, and max_open_connections).

Observability domain options
Direct link to Observability domain options

ObservabilityStorageClickhouse and ObservabilityStorageClickhouseVNext accept the same connection options as ClickhouseStore (url, username, password, or a pre-configured client).

Hosting options
Direct link to Hosting options

ClickHouse runs anywhere you can reach it over HTTP. Two common choices:

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 platforms
Direct 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:

src/mastra/index.ts
import { Mastra } from '@mastra/core'
import { MastraCompositeStore } from '@mastra/core/storage'
import { PostgresStore } from '@mastra/pg'
import { ObservabilityStorageClickhouseVNext } from '@mastra/clickhouse'
import { Observability, DefaultExporter } 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 DefaultExporter()],
},
},
}),
})

Two ways to provision the database:

  • Managed: Use ClickHouse Cloud. Set CLICKHOUSE_URL, CLICKHOUSE_USERNAME, and CLICKHOUSE_PASSWORD as 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.

warning

Do not 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.

Initialization
Direct 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.

Observability
Direct link to Observability

ClickHouse is the recommended backend for production observability:

  • Insert-only strategy: DefaultExporter writes 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 DefaultExporter reference.