Skip to content

Datastores

Langfuse needs four stateful backends. All are bundled by default; all should be external in production.

BackendHoldsLosing it means
PostgreSQLprojects, users, prompts, API keys, configeverything except trace data
ClickHousetraces, observations, scoresall observability history
Redisingestion queue, cachein-flight events; the rest recovers
S3 / MinIOlarge payloads — prompts and completions above the inline limittrace bodies, while metadata survives

Two databases rather than one because the workloads are different: PostgreSQL for transactional metadata, ClickHouse for high-volume append-only analytics over traces.

ClickHouse is single-node here, deliberately

Section titled “ClickHouse is single-node here, deliberately”
langfuse:
clickhouse:
clusterEnabled: false
replicaCount: 1
zookeeper:
enabled: false

The upstream chart defaults to a 3-replica cluster with ZooKeeper. This pack overrides that.

Langfuse runs its schema migrations over the load-balanced ClickHouse Service, and golang-migrate’s schema_migrations bookkeeping is not consistently replicated across the three nodes. Migrations land on different replicas, get marked dirty, and langfuse-web crash-loops.

Single-node avoids that entire class of failure and is far lighter: upstream’s default is three ClickHouse replicas plus a three-node ZooKeeper ensemble, so this is one pod instead of six.

All four use bitnamilegacy/* images pinned by upstream chart 1.5.34.

They also pull from Docker Hub, so restricted egress or rate limiting shows up as image-pull failures:

Terminal window
kubectl -n langfuse get events --field-selector reason=Failed

In a restricted environment, mirror them internally and override the repository, or go external.

examples/prod-external-datastores.yaml has a full example. The shape:

langfuse:
postgresql:
deploy: false
host: postgres.example.com
port: 5432
auth:
username: langfuse
database: langfuse
existingSecret: langfuse-db-credentials
redis:
deploy: false
host: redis.example.com
clickhouse:
deploy: false
host: clickhouse.example.com
s3:
deploy: false
bucket: my-langfuse-bucket

deploy: false removes the bundled subchart and switches the connection settings to the host you supply. Each can be moved independently — a common intermediate step is external PostgreSQL and ClickHouse with bundled Redis and MinIO.

Exact key names belong to the upstream chart; the configuration reference has the full set.

The bundled defaults are development-scale. What actually drives capacity:

  • ClickHouse grows with trace volume, and trace volume grows with LLM call volume. This is the one to plan for; it dominates storage on any active deployment.
  • PostgreSQL stays small — projects, users, prompts, and keys are rows.
  • Redis holds the ingestion queue. It grows when the worker falls behind and drains when it catches up; sustained growth means the worker is under-provisioned.
  • S3 grows with the fraction of payloads exceeding the inline limit, so it scales with prompt and completion size rather than call count.

On a small cluster — fewer than three schedulable nodes — ClickHouse pods commonly sit Pending on CPU or memory:

Terminal window
kubectl -n langfuse get pods -l app.kubernetes.io/name=clickhouse
kubectl -n langfuse describe pod <clickhouse-pod>

Nothing here backs anything up.

BackendBackup
PostgreSQLpg_dump, or your managed service’s snapshots
ClickHouseBACKUP TABLE, or managed snapshots
Redisnone needed — it is a queue
S3 / MinIObucket replication or lifecycle policy

On a Nebari cluster with longhorn-backup-pack, the bundled datastores’ PVCs are covered by the cluster-wide schedule if they sit on the default StorageClass. Confirm rather than assume:

Terminal window
kubectl -n longhorn-system get volumes.longhorn.io \
-l recurring-job-group.longhorn.io/default=enabled

Volume snapshots of a running database are crash-consistent, not application-consistent. Usually fine; a logical dump before a migration is the stronger guarantee.

All four bundled datastores read from langfuse-secrets, and so does Langfuse — one source for both ends of every connection. That is why rotating a password there means restarting the datastore and both Langfuse workloads. See Secrets and GitOps.

External datastores use their own secrets, referenced per-datastore with existingSecret.