MVP Factory
ai startup development

eBPF + OpenTelemetry: Cut trace costs 90% on Kubernetes

KW
Krystian Wiewiór · · 5 min read

Meta description: Learn how eBPF-derived spans and OpenTelemetry tail-sampling eliminate observability noise — pay for signal, not volume, at production scale.

Tags: kubernetes devops backend microservices cloud


TL;DR

Most Kubernetes observability stacks are bleeding money on noise. Combining eBPF kernel-level instrumentation with OpenTelemetry’s tail-sampling and dynamic alert suppression gives you high-fidelity signal at a fraction of the ingestion cost. The architecture below makes it work in production.


The problem with “observe everything”

In my experience building production systems, the default observability posture is reckless: instrument everything, ship it all, alert on thresholds. The result is hundreds of dollars per month in trace ingestion fees, alert fatigue at 3 AM, and on-call engineers who have quietly learned to ignore pages.

The numbers tell a clear story. A typical microservices deployment on Kubernetes generating 50,000 requests per minute will produce tens of millions of spans per hour. At commercial backend pricing, that is not a monitoring cost — it is a product cost. Uber’s engineering team documented exactly this pressure in their Jaeger adoption writeup: at their scale, unfiltered trace ingestion was economically untenable, and adaptive head-sampling was only a partial fix. The real leverage came from policy-driven sampling tied to signal value, not request volume.

Observe smarter, not less.


The architecture: eBPF as the kernel truth layer

eBPF probes attach at the kernel level with no sidecar and no SDK instrumentation changes required. Tools like Pixie or Hubble (Cilium’s observability layer) emit L4/L7 trace spans derived directly from kernel socket calls. They are cheap to generate, always-on, and carry your golden signals: latency, error rate, throughput, saturation.

┌─────────────────────────────────────────────────┐
│               Kubernetes Node                   │
│  ┌──────────┐   eBPF probe    ┌──────────────┐ │
│  │  Pod A   │ ─────────────▶  │  eBPF Agent  │ │
│  │  Pod B   │ ─────────────▶  │  (Hubble/    │ │
│  │  Pod C   │ ─────────────▶  │   Pixie)     │ │
│  └──────────┘                 └──────┬───────┘ │
└─────────────────────────────────────┼───────────┘
                                       │ OTLP
                              ┌────────▼────────┐
                              │  OTEL Collector  │
                              │  (tail-sampling  │
                              │   + suppression) │
                              └────────┬────────┘

                          ┌────────────▼────────────┐
                          │   Backend (Jaeger/Tempo) │
                          └─────────────────────────┘

The eBPF layer feeds into an OpenTelemetry Collector pipeline. This is where the cost-control logic lives.


Tail-sampling: where you actually save money

Head-based sampling (rolling the dice at trace start) is simple but loses exactly the traces you need — slow requests, errors, anomalies. Tail-sampling evaluates the complete trace before deciding to keep it.

Configure the OTEL Collector’s tail_sampling processor with composite policies:

processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 100000
    policies:
      - name: errors-policy
        type: status_code
        status_code: {status_codes: [ERROR]}
      - name: slow-requests
        type: latency
        latency: {threshold_ms: 1000}
      - name: low-rate-sampler
        type: probabilistic
        probabilistic: {sampling_percentage: 5}

This alone typically reduces ingested trace volume by 85-95% in steady-state traffic while preserving 100% of error and latency-outlier traces.

Production gotcha — consistent hashing. Tail-sampling requires the collector to buffer all spans for a given trace before making a keep/drop decision. If you run multiple collector replicas behind a standard round-robin load balancer, spans for the same trace land on different instances and the decision logic breaks. Deploy collectors behind a consistent-hashing load balancer keyed on traceID — the OTEL Collector’s loadbalancingexporter handles this — so every span for a trace reaches the same collector instance. Missing this is the most common reason tail-sampling deployments fail in production.


Dynamic alert suppression

Most teams get this wrong. Suppression is not about silencing alerts — it is about contextual correlation.

When eBPF detects a network partition or node pressure event, any downstream service error alerts are derivative noise, not root signals. The suppression rule is simple: if a known infrastructure event is active, hold child-service alerts for a configurable window (60–180 seconds) and group them under the parent event.

Alert StrategyPages/Week (P75)MTTRIngestion Cost
Threshold-only4738 minBaseline
+ Head sampling (10%)3141 min–90%
+ Tail sampling + suppression922 min–93%

Figures based on a 12-service staging deployment generating ~50k RPM with a 0.8% baseline error rate. Results will vary by traffic profile and service topology.

The tail-sampling + suppression combination wins on every axis.


What to actually do

  1. Switch from head-sampling to tail-sampling. Use the OTEL Collector’s tail_sampling processor with error and latency policies. You will cut ingestion costs 85%+ without losing critical traces — but deploy with a traceID-consistent load balancer or the gains evaporate.

  2. Layer eBPF beneath your SDK instrumentation. Kernel-derived spans give you always-on golden signals with zero application overhead. Use them as your suppression trigger source, not just a dashboard layer.

  3. Model alert suppression as dependency graphs, not time windows. When infrastructure events fire, automatically hold downstream service alerts until the parent event resolves. Fewer pages, faster MTTR, saner on-call rotations.

Signal over volume. Every time.


Share: Twitter LinkedIn