← Back to index

OpenObserve — Internal Architecture

A developer's guide to how OpenObserve actually works under the hood: the node roles, the schema-on-ingest pipeline, columnar Parquet on object storage, and how a query prunes its way down to the few files it actually needs.

Student edition · understand, apply, verify

Follow telemetry from stateful ingestion to immutable analytical files and a versioned query catalog.

What you should be able to do: Explain acknowledgement durability, safe file publication, tenant isolation, and compaction lifetime without calling WAL-owning ingesters stateless.

Architecture tutorial for a student with basic programming knowledge. Defaults, guarantees, file layouts, and APIs are release/configuration-sensitive. Toy models and pseudocode do not implement the named product.

OpenObserve is an observability platform — logs, metrics, and traces in one engine. Its design is built around a single, simple bet: object storage is the only durable store you need. There is no separate time-series database cluster, no dedicated search cluster, and no per-node attached disk that holds the source of truth. Data lands as compressed columnar Parquet files on an object store (S3, GCS, Azure Blob, MinIO, or even a local filesystem), and every other component is effectively a stateless worker that reads and writes those files. That choice — borrowing the data-lake storage model and pairing it with an Apache Arrow / DataFusion query engine — is what makes it cheap to run and is the thread that ties the rest of the architecture together.

Contents

  1. Design Goals and Core Ideas
  2. Architecture
  3. Data Model
  4. Storage Format
  5. Ingestion Path
  6. Indexing & Pruning
  7. Query Path
  8. Compaction & Retention
  9. Summary
  10. Concepts explained
  11. Worked example
  12. Practice and verification

Concepts explained in this guide

Read these definitions first, or use the linked terms as you go. The examples and limitations are local to this guide; companion pages are optional.

Multi-tenancy and publication lifetime

A tenant scopes data/users/quotas/authorization. Immutable publication needs a catalog snapshot and safe file lifetime for readers.

Example. A query using old files must finish before compaction deletes them.

Boundary. Blob upload and catalog publication are separate operations. WAL-bearing ingesters are stateful; actual metadata/backend/visibility behavior is release-sensitive.

Write-ahead log and fsync

A WAL records updates before data pages are considered committed. Recovery replays it after a crash. fsync (or an equivalent storage contract) requests that buffered bytes reach durable storage rather than only process/OS memory.

Example. A server acknowledges after its WAL commit and required replica writes are durably accepted. A process crash can then reconstruct the committed update.

Boundary. Appending to a file alone does not guarantee power-loss durability. Device behavior, filesystem ordering, replicas, corruption, and the declared fault budget still matter.

Partition pruning and predicate pushdown

Pruning excludes storage partitions that cannot match a filter. Column projection reads only requested columns. Predicate pushdown lets file/row-group statistics skip blocks that cannot match; selected blocks still require row filtering.

Example. For a one-day query over a 365-day lake, date pruning can skip 364 days. Min/max time statistics may skip additional row groups only if those statistics are selective.

Boundary. Benefits are not universally independent or multiplicative. Row-group sizes, data clustering, skew, metadata, small files, and storage format determine real bytes read.

Inverted indexes and columnar storage

An inverted index maps searchable terms to document postings. A columnar file groups values by column so analytical scans can read only selected fields. They optimize different access patterns.

Example. 'ERROR' → document IDs {2,7}; 'service=api' → {1,2,7}. Intersection returns {2,7}. A monthly count may instead scan only time, service, and status columns.

Boundary. Tokenization affects meaning, indexes have storage/write cost, and incomplete shard responses must be marked. Search refresh is visibility, not the same as durable commit.

Offsets and checkpoints

An offset is a position in one log partition. A checkpoint is a recoverable, consistent record of input positions and processing state. On restart, state must correspond exactly to the input that is considered processed.

Example. If a batch increments a total and crashes before saving its offset, replay increments twice. Commit the total, dedup state, and partition position together, or use a compatible transactional/idempotent sink.

Boundary. Saving an offset after a write avoids that specific loss window but can still duplicate; framework checkpoints alone do not make arbitrary external effects transactional.

Rollups and weighted aggregation

A precomputed summary per time bucket. Sums and counts merge by addition, min/max by comparison. An average is derived as total sum divided by total count, not an unweighted average of bucket averages.

Example. One window has [10], another [20,20,20]. Merge sum=70 and count=4 to get 17.5, not the unweighted mean of window means (10+20)/2=15.

Boundary. Do not sum cumulative counter snapshots to compute traffic, or average p99 values to get a global p99. Retain reset-aware counter increases or compatible histogram distributions.

1. Design Goals and Core Ideas

Every internal decision in OpenObserve traces back to a small set of goals. Keeping them in mind makes the rest of the architecture predictable.

GoalHow OpenObserve achieves it
Cheap observabilityObject storage is the only required durable store. No separate database cluster, no expensive local SSDs holding the source of truth. Data is compressed columnar Parquet, so storage and scan costs stay low.
Unified signalsLogs, metrics, and traces all flow through one ingestion pipeline and land in the same Parquet-on-object-store format, queried by one engine. There is no separate stack per signal type.
Stateless computeNode roles (router, ingester, querier, compactor) keep no durable state of their own. Everything that must survive a restart lives in the object store or the metadata store, so compute scales independently.
Schema-on-ingestYou do not declare a schema up front. Columns are inferred from the first records and extended as new fields appear, so any JSON shape can be ingested immediately.
SQL-first queryingQueries are SQL (and PromQL for metrics), planned and executed by DataFusion over Arrow. The same engine serves logs, metrics, and traces.
Unified signals on object storage
Logs, metrics, and traces feed one engine; object storage is the only required durable store, which is what makes it cheap to run.

2. Architecture

OpenObserve combines ingestion, query, routing, compaction, and alerting roles with catalog/object storage; exact supported role/backend combinations vary by release. Use current deployment documentation rather than asserting one SQLite/Postgres/MySQL/etcd matrix universally.

WAL/buffer-owning ingesters have state that must survive or replay on replacement. Query workers may be more disposable, but caches, pinned snapshots, tenant state, and control-plane dependencies still matter. Metadata can be a substantial scaling/availability concern, not automatically tiny.

State acknowledgement storage, volume lifecycle, replication/replay, and warm query visibility explicitly. Adding replicas does not automatically recover unflushed local data or preserve the same catalog/authorization guarantees.

3. Data Model

The data model has three levels. An organization is the top-level tenant boundary — it isolates data, users, and quotas. Inside an organization you have streams, which are the equivalent of tables. Each stream has a type: logs, metrics, or traces. A stream is a sequence of records sharing a schema and a retention policy.

Organizations, streams, time partitions
Organizations contain typed streams; schemas are inferred on ingest, and each stream is partitioned by time plus optional partition keys.

Schema inference is what makes ingestion frictionless. You do not run a CREATE TABLE. The first records to arrive on a stream define its columns and their types; when a later record carries a field the schema has not seen, the column is added and old rows simply have a null for it. This is schema-on-ingest — the schema is a living description maintained in the metadata store, not a fixed contract.

Partitioning is what makes querying fast. Every stream is partitioned by time (records are grouped into time buckets, typically hourly) and optionally by one or more partition keys (for example, a service or k8s_namespace column). Partitioning is expressed in the object-store key layout — each partition is its own prefix:

# object key layout for a partitioned stream
org/acme/logs/app_logs/
  2026/06/25/10/service=api/  # time bucket + partition key
    7f3a...parquet
    9b21...parquet
  2026/06/25/10/service=web/
    a44c...parquet
  2026/06/25/11/service=api/
    ...

Because partitions are encoded in the key prefix, a query with a time range and a service filter can be narrowed to a handful of prefixes before any file is opened. That is the first and cheapest layer of pruning.

4. Storage Format

Parquet groups column chunks into row groups and encodes/compresses them. Readers can project columns and use available statistics/bloom/index metadata to prune. Fetching metadata/footers itself can require I/O; 'before any bytes' is not literal unless that metadata was already cached.

Arrow is a memory layout; Parquet is an encoded persistent format. Decoding/decompression/conversion has cost, so do not describe it as a zero-conversion mapping. Compression ratios and min/max selectivity are workload-dependent. Keep immutable file metadata/length/checksum/version and the catalog publication boundary coherent.

5. Ingestion Path

Validate tenant, envelope, timestamps, schema, and sensitive-data policy. Acknowledgement follows the configured durable ingestion boundary; append in memory/OS cache alone is not fsync or host-loss protection. Retain recoverable input until its required object/catalog publication completes.

Convert/validate bounded batches to immutable Parquet objects; upload with idempotent version/run identity; then register a committed file manifest/catalog entry through a supported transaction. Blob upload plus DB update is not one atomic external action. Crash gaps need retry/reconciliation/orphan cleanup and no premature WAL truncation.

Handle schema drift/type conflicts under an explicit policy, not always by silently adding nullable columns. In-memory/WAL/remote query paths and latency depend on release/settings. Pseudocode is an architecture sketch, not a guaranteed implementation of all versions.

6. Indexing & Pruning

OpenObserve does not scan everything. The whole performance story is about reading as few bytes from the object store as possible, because object-store reads are the slow and metered part of a query. It does this with layered pruning, from coarse to fine.

Indexing and pruning candidate files
The pruner uses partition prefixes, file-level min/max stats, bloom filters, and inverted indexes to drop files that cannot match before any are read.

The layers, applied in order:

LayerGranularityWhat it eliminates
Partition pruningprefix / fileTime-range and partition-key filters narrow the candidate set to a few object-store prefixes before any file is touched.
Min/max column statsfile & row groupEach Parquet file (and each row group) carries per-column min/max. If a filter's value falls outside a file's range, the whole file is skipped.
Bloom filtersfileFor high-cardinality equality filters (e.g. a specific trace_id), a bloom filter says "this value is definitely not here" so the file is skipped without reading it.
Inverted indexterms within a fileFor full-text search over log fields, an inverted index maps terms to the records that contain them, so a text query reads only matching rows rather than scanning the column.

Cached catalog statistics can prune before column-data fetch; footers/indexes may still require metadata I/O. Measure actual bytes and selectivity.

7. Query Path

A query is SQL (or PromQL for metrics). It is parsed into a logical plan, pruned down to a file set, then executed in parallel by queriers that fetch only the needed Parquet from the object store and run the plan with DataFusion over Arrow record batches.

Query path: plan, prune, fetch, execute
SQL/PromQL is planned and pruned, then scattered across queriers that pull only the needed files from the object store and execute on Arrow.

The end-to-end flow is:

function query(sql):
  plan  = planner.build(sql)              # logical plan + filters
  files = prune(plan, metadata)           # partitions + min/max + bloom
  shards = split(files, num_queriers)     # scatter the file set
  partials = []
  for q in queriers:                      # run in parallel
    partials += q.execute(plan, shards[q])
  return merge(plan, partials)            # gather + final aggregation

# inside a single querier:
function execute(plan, files):
  for f in files:
    cols  = f.read_columns(plan.projected_columns)  # only needed columns
    batch = arrow.decode(cols)            # Parquet -> Arrow
    emit(datafusion.run(plan, batch))     # filter / aggregate

Two things keep this fast. First, scatter-gather: in HA mode the pruned file set is split across many queriers, so a large scan parallelizes across machines and the partial results are merged at the end. Second, columnar projection: each querier reads only the columns the query references out of each Parquet file, so a SELECT count(*) WHERE service='api' never pays to read the log body. DataFusion does the actual relational work — filtering, joining, aggregating — on Arrow batches, which are a columnar in-memory format that maps almost directly onto Parquet.

8. Compaction & Retention

Merge/retention uses validated immutable output plus atomic catalog replacement and safe old-file lifetime. A query planned against old files must not lose them mid-read. Use the supported snapshot/pinning/grace and bounded-reader protocol, not immediate delete after two separate catalog calls.

Deduplication needs a stated stable identity/window policy; merging is not inherently dedup. Metrics downsampling must preserve reset-aware counters, weighted means, and compatible distributions for quantiles. Retention applies to actual timestamp/content rules and file boundaries; partial expired rows may require rewrite rather than a universal prefix delete.

Capacity, recovery, authorization, and tenant quotas apply during backfills/compaction too. Isolate expensive queries and monitor freshness, lag, publication errors, and retained reader age.

Worked example: compaction meets an in-flight query

Use this example to check which promise each mechanism actually provides.

compaction meets an in-flight queryQuery pins catalog version V1: It is reading old files A and B. Tenant/time/access filters are part of planning.. Compactor writes new file C: C is immutable and validated before publication. Upload and metadata registration are separate failure boundaries.. Publish catalog V2 atomically: New queries see C. Old V1 queries still require A/B until their lifetime/pin expires under a supported protocol.. Delete old files safely: Retain objects required by readers/recovery. Immediate deletion after registration can break an old query; metadata pointers alone do not protect its files.1Query pins catalog version V12Compactor writes new file C3Publish catalog V2 atomically4Delete old files safely
Sequence for this example. The table explains the evidence and decision at each step.
StepEvidence, decision, and expected result
Query pins catalog version V1It is reading old files A and B. Tenant/time/access filters are part of planning.
Compactor writes new file CC is immutable and validated before publication. Upload and metadata registration are separate failure boundaries.
Publish catalog V2 atomicallyNew queries see C. Old V1 queries still require A/B until their lifetime/pin expires under a supported protocol.
Delete old files safelyRetain objects required by readers/recovery. Immediate deletion after registration can break an old query; metadata pointers alone do not protect its files.
Check your understanding: Is local WAL append sufficient to survive loss of the ingester host?

Not by itself. fsync/storage behavior and persistent volume/replication/recovery placement define that fault budget. Local acknowledged data may be lost if its only durable host is destroyed.

Run and change the example

Download guide_labs.py. Python 3.10+, standard library only. In its directory, run:

python3 guide_labs.py publication
python3 guide_labs.py test

The model separates object existence, catalog publication, reader pins, and safe cleanup.

The program tests its stated finite contracts. It is an educational model, not an implementation of the named production product or a proof of unmodeled distributed faults.

Sources and further verification

9. Summary

The whole system is built from a few reinforcing ideas:

ConcernMechanism
Where does data live?Compressed columnar Parquet on an object store — the only required durable store. The metadata store holds just the catalog.
How is it organized?Organizations contain typed streams (logs/metrics/traces); each stream is partitioned by time plus optional partition keys.
How does ingest stay fast?Append to WAL, ack, buffer in memory, then flush asynchronously to Parquet. Schema is inferred on ingest, never declared.
How are reads so cheap?Layered pruning — partition prefixes, file min/max stats, bloom filters, inverted index — drops files on metadata alone, then reads only the needed columns.
How does it execute?SQL/PromQL planned by DataFusion, scattered across stateless queriers, executed on Arrow batches, gathered at the end.
How is storage reclaimed?The compactor merges small files, downsamples old metrics, and deletes expired partitions by prefix — all rewrite-and-replace in the catalog.
How does it scale?Roles are stateless; add ingesters for write load, queriers for read load. Data and catalog are untouched.
The recurring theme: object storage is the source of truth, compute is stateless and disposable, and every operation is rewrite-and-replace plus metadata-driven pruning. There is no database cluster to scale, no in-place mutation to lock, and no expensive index that has to hold the whole dataset in memory.

⌂