Loki vs Elasticsearch vs VictoriaLogs: The Honest Log Aggregation Comparison

Your logs are eating you alive. You started with a single docker logs pipe into a file. Then you added a couple of services. Then a couple became a dozen, and now you’re either squinting at journalctl across ten SSH sessions or paying $800/month to Datadog for the privilege of having a search box. Neither is a good place to be.

The self-hosted log aggregation space has three serious contenders right now: Grafana Loki, Elasticsearch (the backbone of the ELK stack), and the relative newcomer VictoriaLogs. Each solves the same problem with a completely different philosophy, and choosing wrong will cost you — either in RAM, in dollars, or in the slow agony of a query language that fights you every step of the way.

This is a working comparison from running all three in real environments. No marketing copy, no cherry-picked benchmarks from the vendor’s own blog.


The Philosophy Gap Is Bigger Than You Think

Before diving into numbers, you need to understand why these tools differ, because it changes everything downstream.

Elasticsearch indexes everything. Every field, every word, every token gets written into an inverted index. This makes arbitrary full-text search blindingly fast — but it also means you pay the indexing cost upfront, always, for every byte that arrives. Memory usage is not optional; Elasticsearch needs a substantial heap, and it gets grumpy if you starve it.

Loki does the opposite. It indexes only metadata labels (think: app=nginx, namespace=production, host=web-01). The actual log content is stored compressed and unindexed. Queries work by filtering on labels first, then doing a brute-force scan of compressed chunks. The model is explicitly inspired by Prometheus — if you already think in labels, it clicks immediately. If you don’t, it’s a rough adjustment.

VictoriaLogs sits between them, philosophically. It uses a compressed columnar storage format and indexes a small set of "stream fields" (similar to Loki labels), but it can also do reasonably fast full-text search across log messages without pre-indexing everything. It launched stable in 2024 and the team has been shipping fast.


Cost: What Actually Runs Up Your Bill

Storage

Loki wins here, and it’s not close for most workloads.

Loki stores compressed log chunks. Real-world compression ratios on typical application logs (JSON, structured, with repeated keys) run 10:1 to 20:1. 100 GB of raw logs becomes 5-10 GB on disk. If you’re using S3 or an S3-compatible backend (MinIO, Wasabi, Backblaze B2), costs crater.

Elasticsearch stores both the raw source documents and the inverted index. Depending on how many fields you index and how high your cardinality is, the index overhead ranges from 30% to 300% on top of the raw data size. A 100 GB raw log set might consume 150-400 GB in Elasticsearch. There are ways to tune this (ILM policies, frozen indices, rollups), but you’re fighting the architecture.

VictoriaLogs compresses aggressively — comparable to Loki in practice, sometimes better on highly repetitive logs. Storage is a genuine strength.

Memory

This is where Elasticsearch gets painful to operate at scale. The JVM heap needs to be set correctly (not too low, not above half your physical RAM for OS file cache). For anything production-grade, you’re looking at 16-32 GB RAM minimum for a multi-node setup. The file cache on top of that is what makes reads fast.

Loki’s memory footprint for the querier and ingester is substantially lower. A small single-node Loki deployment can run on 2-4 GB RAM and handle serious log volumes. The catch is that query performance degrades badly if you scan huge time ranges without tight label filters.

VictoriaLogs is the memory efficiency champion right now. The VictoriaMetrics team has a pathological focus on low resource usage, and it shows. 1-2 GB RAM handles workloads that would need 8+ GB in Elasticsearch. For self-hosted environments with resource constraints, this is a compelling argument.

Operational Complexity Cost (the hidden one)

Running an Elasticsearch cluster means dealing with shard allocation, JVM tuning, snapshot management, and the occasional red cluster that wakes you up at 3 AM. The Elastic operator for Kubernetes has gotten better, but it’s still a significant operational surface.

Loki in "simple scalable" mode is more approachable, but the multi-component architecture (ingester, querier, distributor, compactor, ruler) adds its own surface area. Loki’s monolithic mode (-target=all) is fine for small deployments and easy to run.

VictoriaLogs ships as a single binary with no external dependencies. One process, one config file, done. For a homelab or a small team, this is legitimately refreshing.


Query Speed: Where the Rubber Meets the Road

Benchmarks are inherently synthetic. Here’s what matters in practice.

Elasticsearch

Arbitrary full-text search: fastest of the three. If you need to find a specific error string across six months of logs without knowing which service generated it, Elasticsearch handles this without breaking a sweat. The inverted index does exactly what it’s designed for.

Aggregations are also fast — computing cardinality, grouping by arbitrary fields, building histograms. This is where the ELK stack’s analytics capabilities shine.

Loki

Narrow time range + precise labels: fast. Wide time range + no labels: agonizingly slow. This is the Loki experience in one sentence.

The architecture forces you to design your labels upfront and stick to them. If you do, log queries respond in seconds. If you try to run a label-less grep across 30 days of logs, you’re going to get a timeout or wait several minutes.

LogQL has improved significantly. The |= "string" filter, | json parser, pattern matching, and metric queries are genuinely powerful once you learn them. But the learning curve is real, and the error messages when you get something wrong are often unhelpful.

VictoriaLogs

Full-text search without pre-indexing: surprisingly competitive. The columnar storage format and block-level filtering mean it can scan log content faster than expected for an append-only store. It won’t beat Elasticsearch on cold full-text searches across massive datasets, but it closes the gap significantly.

The query language is LogsQL. It’s simpler than both LogQL and Elasticsearch’s Query DSL. error finds logs containing "error". _stream:{app="nginx"} AND status:5* filters by stream and prefix-matches fields. It’s intuitive fast enough that you can start being productive in an hour.

One actual benchmark worth citing: the VictoriaMetrics team has published comparisons showing VictoriaLogs querying 1 TB of logs faster than Loki on equivalent hardware, primarily because the columnar format is better at skipping irrelevant data. Take vendor benchmarks with appropriate skepticism, but the architecture does support the claim.


Ergonomics: Living With the Tool Daily

The Grafana Factor

All three integrate with Grafana. Loki has the tightest integration — it’s from the same company, the Explore interface has a dedicated log panel, and LogQL is well-understood by the Grafana team. If you’re already running Prometheus + Grafana, adding Loki is nearly frictionless.

Elasticsearch connects via the official Grafana data source plugin. It works, but it’s clunky — you often end up in Kibana for serious log investigation because Grafana’s Elasticsearch integration is a layer removed.

VictoriaLogs supports both the Loki HTTP API (compatible with Grafana’s Loki data source) and has its own UI via VMUI. The Loki API compatibility is a big practical win — you can point an existing Loki data source at VictoriaLogs and most things just work.

Ingestion: Getting Logs In

Loki ingests via its own push API, and the standard client is Promtail (being superseded by Grafana Alloy). If you’re on Kubernetes, the Helm chart works well. For Docker, the Loki Docker log driver is convenient but has caused issues with buffering under high load — use Promtail or Alloy to collect from container logs instead.

Elasticsearch takes logs from Logstash, Beats (Filebeat, Metricbeat), or directly via its HTTP API. The Beats ecosystem is mature and handles most sources. Logstash is powerful but resource-hungry — for lightweight collection, Filebeat directly to Elasticsearch is the better path.

VictoriaLogs accepts data from Filebeat (via the Elasticsearch-compatible HTTP API), Logstash, Fluentd/Fluent Bit, Promtail, Vector, and OpenTelemetry. The multi-protocol support is excellent for migrating from other stacks.


Practical Setup: Docker Compose Examples

Loki + Promtail (Minimal)

# docker-compose.yml — Loki single-node + Promtail
version: "3.8"

services:
  loki:
    image: grafana/loki:3.4.3
    container_name: loki
    ports:
      - "3100:3100"
    volumes:
      - ./loki-config.yaml:/etc/loki/loki-config.yaml
      - loki_data:/loki
    command: -config.file=/etc/loki/loki-config.yaml
    restart: unless-stopped

  promtail:
    image: grafana/promtail:3.4.3
    container_name: promtail
    volumes:
      - /var/log:/var/log:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - ./promtail-config.yaml:/etc/promtail/promtail-config.yaml
    command: -config.file=/etc/promtail/promtail-config.yaml
    restart: unless-stopped

volumes:
  loki_data:
# loki-config.yaml
auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9096

common:
  instance_addr: 127.0.0.1
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

query_range:
  results_cache:
    cache:
      embedded_cache:
        enabled: true
        max_size_mb: 100

schema_config:
  configs:
    - from: 2025-01-01
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

# Increase default limits to avoid frustrating timeouts
limits_config:
  query_timeout: 5m
  max_query_parallelism: 32
  ingestion_rate_mb: 64
  ingestion_burst_size_mb: 128

VictoriaLogs (Dead Simple)

# docker-compose.yml — VictoriaLogs standalone
version: "3.8"

services:
  victorialogs:
    image: victoriametrics/victoria-logs:v1.22.0
    container_name: victorialogs
    ports:
      - "9428:9428"   # main HTTP API
    volumes:
      - victorialogs_data:/vlogs
    command:
      - -storageDataPath=/vlogs
      # Retention: keep 30 days by default
      - -retentionPeriod=30d
      # Accept Loki push API — point Grafana Loki data source here
      - -syslog.listenAddr.tcp=:5514
    restart: unless-stopped

  # Vector as a lightweight shipper — handles Docker logs
  vector:
    image: timberio/vector:0.44.0-alpine
    container_name: vector
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./vector.toml:/etc/vector/vector.toml:ro
    restart: unless-stopped
    depends_on:
      - victorialogs

volumes:
  victorialogs_data:
# vector.toml — collect Docker logs and ship to VictoriaLogs
[sources.docker]
type = "docker_logs"

[transforms.parse_json]
type = "remap"
inputs = ["docker"]
source = '''
  # Try to parse JSON logs; leave as-is if they're plain text
  structured, err = parse_json(.message)
  if err == null {
    . = merge(., structured)
  }
'''

[sinks.victorialogs]
type = "elasticsearch"
inputs = ["parse_json"]
endpoints = ["http://victorialogs:9428/insert/elasticsearch/"]
mode = "bulk"
# VictoriaLogs uses these fields to build log streams
query.extra_fields = "host:{{ host }},app:{{ .container_name }}"

Gotchas

Loki: Cardinality will kill you. Do not use high-cardinality values as labels. User IDs, request IDs, IP addresses as labels — your indexer will OOM or grow to an unmanageable size. Labels are for low-cardinality identifiers: service name, environment, host name. Everything else belongs in the log line and gets filtered at query time.

Loki: The max_look_back_period trap. Loki’s default query range limits are surprisingly aggressive. If users hit "no results" on queries they expect to work, check query_timeout, max_query_lookback, and query_ingesters_within. Tuning these is underdocumented.

Elasticsearch: Heap sizing is mandatory and non-obvious. Set ES_JAVA_OPTS=-Xms4g -Xmx4g (same value for both to avoid heap resizing pauses). Never exceed 50% of system RAM — the other half is needed for the OS file cache that Lucene depends on. Getting this wrong means bad performance at best, OOM kills at worst.

Elasticsearch: Index template management is a part-time job. As your schema evolves, fields that were one type in old indices become a different type in new ones. Mapping conflicts are silent until they explode during a query. Use ILM policies and explicit index templates from day one.

VictoriaLogs: It’s young. The 1.x stable release is solid, but the ecosystem is thinner than Loki or Elasticsearch. Some edge cases in the Loki API compatibility surface occasionally. Check the GitHub issues before committing to it for a critical production use case, and allocate time to verify your specific ingestion pipeline actually works.

VictoriaLogs: No native clustering yet. The current architecture is single-node. For very high-volume environments (tens of GB/day), this is a real constraint. VictoriaMetrics cluster mode exists for metrics; a cluster mode for logs is on the roadmap but not shipped as of mid-2026.


Production-Ready Practices

Retention and cost: Set explicit retention periods. Loki’s compactor handles deletion based on retention_period in the config. VictoriaLogs has -retentionPeriod. Elasticsearch uses ILM. Without this configured, disks fill silently.

Alerting on ingestion lag: Your logging pipeline is worthless if it silently stops ingesting. Monitor the delta between log event timestamp and ingestion timestamp. Prometheus metrics are available for all three — alert when lag exceeds 5 minutes.

Object storage for Loki at any serious scale: Local filesystem storage for Loki works fine for proof-of-concept. For production, use MinIO or S3. The s3 object store config in Loki is well-documented and the performance is good. This also gives you disaster recovery for free.

Parse at ingest, not at query: Whether you’re using Logstash, Vector, or Fluent Bit, extract structured fields during ingestion. Running | json on every Loki query instead of ingesting pre-parsed JSON wastes CPU on every query. Structured logs are faster everywhere.


The Verdict

Pick Elasticsearch if: You need rich full-text search across arbitrary fields with no query design constraints. You have the hardware budget (16+ GB RAM comfortably), want the mature Kibana UI, and need to run complex analytical queries against historical log data. The ELK stack is battle-tested and the tooling ecosystem is the richest of the three.

Pick Loki if: You’re on Kubernetes, you’re already running Prometheus and Grafana, and you can discipline your team to use label-based querying. The storage costs are genuinely excellent. The Grafana integration is seamless. If your logs are structured and your labels are clean, LogQL is a pleasure to write.

Pick VictoriaLogs if: You want the lowest possible resource footprint, hate operational complexity, and are migrating from either Loki or Elasticsearch (the multi-protocol support makes this practical). For small to medium self-hosted setups — a handful of services, a homelab, a startup’s internal infrastructure — it punches well above its weight and is the easiest to operate.

For the majority of self-hosted scenarios, VictoriaLogs is the right default choice today. The operational simplicity, storage efficiency, and query ergonomics are hard to beat when you’re not running a team dedicated to the observability stack. Loki is excellent if you’re already deep in the Grafana ecosystem and your label discipline is tight. Elasticsearch earns its place in large organizations that need its analytical depth and can staff it properly.

The worst outcome is picking Elasticsearch because it’s "the enterprise choice" and then spending three weekends tuning shard allocation and JVM settings for a deployment that would have run fine on a 4-core VictoriaLogs instance.


Further reading:

👁 Views: 112,655 · Unique visitors: 45,393