Learn
Hardware Requirements

Hardware Requirements

How much CPU, RAM, and disk Traceway needs depends almost entirely on which storage backend you run. The backend ships as a single Go binary with the frontend embedded (static and CGO-free except in DuckDB mode, which links the DuckDB engine), so the binary itself is cheap. What costs memory is the database next to it.

There are three shapes:

  • SQLite mode (single binary, zero external dependencies). The whole stack is one process. This is the lightweight option.
  • DuckDB mode (telemetry_duckdb build tag, CGO required). Still a single container with zero external dependencies, but the telemetry store is DuckDB's columnar engine, which holds 10 to 100 times more queryable rows than SQLite on the same box. The catch: DuckDB sizes itself from host RAM, so memory-capped containers must set DUCKDB_MEMORY_LIMIT.
  • ClickHouse + PostgreSQL mode (transactional_pg telemetry_ch build tags). ClickHouse is built to use a lot of RAM for caches and merges, so this is the heavier option that scales much further. It is also the only mode that supports running more than one backend instance; see Running more than one instance.

TL;DR

DeploymentMinimum to startComfortableNotes
SQLite (single binary)1 vCPU, 512 MB RAM2 vCPU, 1 to 2 GB RAMThe backend process idles at ~30 MB RSS. Memory grows with the SQLite page cache and source map cache, both capped.
DuckDB (single container)2 vCPU, 2 GB RAM, DUCKDB_MEMORY_LIMIT set2 to 4 vCPU, 4 to 8 GB RAMDuckDB auto-tunes to ~80% of what it thinks is host RAM. In a memory-capped container it reads the host, not the cgroup, so set DUCKDB_MEMORY_LIMIT explicitly or the backend gets OOM-killed under ingest.
ClickHouse + PostgreSQL2 vCPU, 4 GB RAM4 vCPU, 8 GB+ RAMClickHouse is the floor here, not Traceway. It claims up to 80% of host RAM by design.

If you just want "the smallest box that runs it," that is the SQLite deployment, and it is genuinely small. The Go process is not where your money goes.

The backend process itself

The backend is a single static binary with the SvelteKit frontend embedded into it. There is no Node runtime, no separate web server, and (in SQLite mode) no external database to host.

Measured at idle in SQLite mode, serving the dashboard and API with every background worker running (notification evaluator, retention worker, the 32-worker session-recording uploader, OAuth prune worker):

Idle RSS ≈ 30 MB

So the intuition that "it's about 100 MB" is, if anything, conservative for an idle instance. The process baseline is small. What makes the number move is data flowing through it, and that load lands in a few well-defined caches, each with a configurable ceiling.

What consumes memory under real traffic:

  1. The SQLite page cache (SQLite mode only), capped at ~512 MB.
  2. The source map cache, capped at 500 MB in memory by default, and only filled if you upload source maps.
  3. Per-request conversion work: decoding OTLP/JSON payloads and hashing, proportional to your ingest rate. This is CPU, not retained memory.

None of these are committed up front. A box with no source maps and light ingest stays near the idle floor.

SQLite mode (the small one)

The SQLite deployment is one Alpine container, one process, two SQLite files, and a folder for blobs. No Postgres, no ClickHouse.

A 1 vCPU / 512 MB box is enough to stand it up and run a small project. 2 vCPU / 1 to 2 GB is comfortable for a real workload and leaves headroom for the caches.

The telemetry database is tuned for append-heavy writes and will grow its page cache up to ~512 MB under sustained ingest (cache_size(-524288)), with a 1 GB memory-mapped read window on top. On a small box you do not need to touch this. The cache is a ceiling, not a reservation, and SQLite only grows it as the working set demands.

We benchmark the single-binary SQLite build on dedicated Hetzner CCX instances, from CCX13 (2 vCPU / 8 GB) up to CCX43 (16 vCPU / 64 GB). Even the small tiers sustain tens of thousands of OTLP data points per second over OTLP/HTTP before write latency starts to climb, which is far more headroom than most self-hosted projects need.

Where does SQLite actually stop being enough? We pushed the single-binary build until it fell over and wrote up exactly where the cliff is and why: Breaking SQLite Traceway (opens in a new tab). When you outgrow it, the DuckDB mode below keeps the single-container shape with far more read headroom, and the ClickHouse + PostgreSQL deployment is the full scale-out path.

Disk

SQLite stores telemetry on local disk, so disk is the resource to watch, not RAM. Size it for your retention window. Telemetry rows are pruned by a background worker (SQLITE_RETENTION_DAYS, default 30); see Data retention on the SQLite page. Blob storage (source maps, session recordings, AI traces) lives under the same /data volume unless you offload it to S3.

DuckDB mode (the small one that reads big)

The DuckDB deployment keeps the single-container shape: the main DB stays SQLite, only the telemetry store becomes DuckDB. You get the same zero-dependency operations with dramatically more read headroom - on a 2 vCPU / 8 GB box the metrics dashboard stays under the 5 s threshold at 100M stored points, where SQLite cliffs around 1M.

Two things differ from the SQLite sizing story:

  • Memory is the knob to set, not to ignore. DuckDB auto-tunes its memory budget to ~80% of RAM, and inside a container it sees the host's RAM, not the container limit. On any memory-capped deployment, set DUCKDB_MEMORY_LIMIT (for example 2GB on a 4 GB container) and optionally DUCKDB_THREADS. An uncapped DuckDB on a capped container is the one configuration that reliably OOM-kills the backend.
  • The image is Debian, not Alpine. The DuckDB driver bundles glibc static libraries, so this build cannot run on musl-based images.

Under sustained ingest, DUCKDB_CHECKPOINT_THRESHOLD (default 16MB) can be raised to reduce WAL checkpoint stalls at the cost of a larger WAL and longer restart replay.

Disk behaves like the SQLite story above (same retention worker, configured with DUCKDB_RETENTION_DAYS, same /data volume), but columnar compression makes the telemetry file several times smaller for the same rows.

ClickHouse + PostgreSQL mode

The Docker Compose, all-in-one, and minimal deployments run the transactional_pg telemetry_ch build against ClickHouse (telemetry) and PostgreSQL (config). Here the hardware floor is set by ClickHouse, not by the Traceway backend.

ClickHouse is designed to use memory aggressively. In the bundled config it claims up to 80% of host RAM (max_server_memory_usage_to_ram_ratio = 0.8) for query and merge buffers, and reserves a multi-GB mark cache. Plan around that, not around the Go process:

  • Minimum: 2 vCPU / 4 GB RAM. Workable for a small instance, but ClickHouse will feel tight.
  • Comfortable: 4 vCPU / 8 GB+ RAM. Give ClickHouse room for merges so background compaction keeps up with ingest.
  • Heavy ingest: 8 vCPU / 16 to 32 GB. At this point you are scaling ClickHouse, and the backend stays comparatively cheap.

PostgreSQL holds only relational config (users, projects, organizations, notification rules) and is low-volume. The backend opens a small pool (25 connections) against it, and it is never the bottleneck.

For a deployment that points the backend at a managed or external ClickHouse instead of co-locating it, the SUT only needs to host the Go backend plus local Postgres, which brings its own footprint back down close to the SQLite numbers.

When you are deciding whether to scale ClickHouse, scale the backend, or put a queue in front of ingestion, the Capacity & Self-Monitoring guide walks through the exact metrics and thresholds for each.

What actually drives memory (and how to cap it)

If you are sizing a small box, these are the knobs that matter. Every one has a hard ceiling, so the process cannot grow unbounded.

DriverDefaultTune withWhen it matters
Source map cache (in memory)500 MB cap, 200 resolversSOURCEMAP_CACHE_MAX_BYTES_MB, SOURCEMAP_CACHE_MAX_ENTRIESJS/TS projects that upload source maps. On a 1 to 2 GB box, set the cap to 100.
Source map cache (disk tier)offSOURCEMAP_CACHE_TYPE=disk, SOURCEMAP_DISK_CACHE_MAX_MB (2048)Many active maps, or multiple backend instances. Moves .tw resolvers to memory-mapped files.
SQLite page cache~512 MB capcache_size pragmaSQLite mode under sustained ingest. A ceiling, grown on demand.
DuckDB memory budget~80% of host RAMDUCKDB_MEMORY_LIMIT, DUCKDB_THREADSDuckDB mode. Mandatory in memory-capped containers - DuckDB sizes itself from the host, not the cgroup.
Session-recording uploader32 workers, 2048 queueSESSION_RECORDING_UPLOAD_WORKERS, SESSION_RECORDING_UPLOAD_QUEUE_SIZEBrowser/mobile session replay. Set workers to 0 to disable entirely.

On a memory-constrained host, the single most effective change is lowering SOURCEMAP_CACHE_MAX_BYTES_MB. The source map cache is the largest tunable consumer, because resolving a minified stack frame means holding a built resolver in memory.

Source maps are the heaviest single thing the backend keeps in memory, and the caching design (in-memory LRU, the precompiled .tw format, the optional disk tier) exists specifically to keep that bounded. The full story is in A deep dive into the source maps (opens in a new tab), with the in-product mechanics on the Symbolication (JS) page.

Rules of thumb

  • "I want the cheapest thing that runs." SQLite mode, 1 vCPU / 512 MB. It idles around 30 MB and the caches are capped.
  • "Small production app, some JS source maps." SQLite mode, 2 vCPU / 1 to 2 GB, source map cap at 100 to 250 MB.
  • "Single container, but the dashboards got slow as data grew." DuckDB mode, 2 to 4 vCPU / 4 to 8 GB, with DUCKDB_MEMORY_LIMIT set to roughly half the container's RAM.
  • "High ingest, long retention, want it to scale." ClickHouse + PostgreSQL, 4 vCPU / 8 GB+, and size ClickHouse first.
  • "It's getting slow." Don't guess. Turn on self-monitoring and let the metrics tell you whether to scale ClickHouse, scale the backend, or buffer ingestion.