OpenTelemetry Profiling Signal: Continuous Profiling with eBPF and OTLP (2026)

OpenTelemetry Profiling Signal: Continuous Profiling with eBPF and OTLP (2026)

OpenTelemetry Profiling: The Fourth Signal, eBPF and OTLP Explained

Every observability stack eventually hits the same wall. Traces tell you which service was slow, metrics tell you that CPU saturated at 14:02, and logs tell you something threw an exception. None of them tell you which function, on which line, burned the cycles. Teams fill that gap with a separate continuous profiler, a separate agent, a separate wire format and a separate UI, and then try to join it all back together by hand.

OpenTelemetry profiling is the project’s attempt to end that split. In March 2026 the Profiling SIG announced that the Profiles signal had entered public alpha, with a dictionary-based OTLP data model, a lossless pprof translator, and Elastic’s donated eBPF profiler running as a Collector receiver. As of this writing in October 2026 it is still alpha, which shapes everything below.

You will leave with a precise mental model of the OTLP profiles wire format, how the eBPF profiler gets stacks without touching your processes, how profiles join to traces, what is and is not production-ready, and a runnable Collector and Kubernetes starting point.

What this covers: the state of the signal, the reference pipeline, the data model field by field, trace correlation, deployment config, failure modes, and an adoption checklist.

Context and Background

Profiling is older than observability. Tools such as perf, gprof and the JVM’s own samplers have answered “where does the time go” for decades. What changed in the cloud-native era is the delivery model. Continuous profiling means sampling production processes all the time at low frequency, storing the results, and querying them like any other telemetry. Grafana Pyroscope, Parca, Datadog’s profiler and Google’s older Cloud Profiler all follow that pattern, and each historically defined its own ingestion protocol or leaned on Google’s pprof format.

pprof is the lingua franca. The Google pprof profile.proto defines a Profile with a string_table (entry zero must be the empty string), arrays of Sample, Location, Function and Mapping messages that reference each other by numeric ID, and a sample_type list that says what each sample value means, for example CPU nanoseconds or allocated bytes. In a pprof sample the stack is a list of location IDs with the leaf at index zero. It is a good format, but it was designed for one-shot capture of a single process, not for a streaming fleet pipeline with resource attributes, schema versioning and trace linkage.

OpenTelemetry’s profiling effort started with the SIG formed at KubeCon Europe 2022 in Valencia. According to the project’s own March 2024 post, the milestones were a vision alignment in August 2022, a first data model proposal in September 2023, a second in November 2023, and a merged data model OTEP in March 2024. That same post recorded Elastic’s pledge to donate its eBPF-based profiling agent, pending due diligence, and Splunk starting to donate its .NET profiler. Named SIG contributors came from Datadog, Google, Grafana Labs and Red Hat.

Why does that matter to a platform team? Because the three existing signals already share a transport (OTLP, the OpenTelemetry Protocol), a resource model and a Collector. If profiles ride the same rails, then enrichment, routing, sampling and redaction you already operate apply to a fourth signal. If you run a Collector estate, the cost of adding profiles is mostly configuration rather than a new agent fleet. The same tail-sampling and cost control disciplines apply, as covered in our guide to OpenTelemetry tail sampling and trace pipeline cost control.

One caveat frames the rest of this article. The official alpha announcement states the signal should not be used for critical production workloads, and that production-ready backends have not yet emerged. This is an evaluation-window technology, and the honest engineering stance is to learn it now and adopt it deliberately. Primary sources are the Profiles Enters Public Alpha post and the profiles concepts page.

How OpenTelemetry Profiling Works: Reference Architecture

In OpenTelemetry profiling, a whole-system eBPF agent samples stack traces from every process on a Linux host, a Collector pipeline enriches them with resource and Kubernetes metadata, and the OTLP profiles format ships deduplicated stacks to a backend. Profiles can carry trace and span IDs so a slow request links to the code that ran.

OpenTelemetry profiling pipeline from eBPF sampling through OTLP profiles to a backend

Figure 1: The OpenTelemetry profiling pipeline, from kernel sampling to a profile backend.

Figure 1 shows the path a single sample takes. A timer-driven perf event fires on each CPU at the configured frequency. An eBPF program attached to that event walks the stack inside the kernel, writes the raw frame addresses to a map, and a user-space component reads those maps, resolves what it can, caches frames and emits OTLP. The same Collector then applies processors and exports. Nothing in this path is loaded into the profiled process, which is the key operational property.

The eBPF profiler is a Collector receiver, not a sidecar

The donated agent now lives at open-telemetry/opentelemetry-ebpf-profiler. Its README lists supported runtimes: C and C++, Go, Rust, Zig, Python, Java on the Hotspot JVM, Node.js and V8, .NET, PHP, Ruby, Perl and Erlang. It targets amd64 and arm64. It is Apache-2.0 licensed, with the eBPF source itself under GPL 2.0, a split that matters to legal review because the kernel verifier requires GPL-compatible programs for certain helpers.

The alpha announcement says it ships as an official Collector distribution and works as a Collector receiver. The README is blunt about packaging: the standalone binaries are unsupported, may be dropped, and should not be deployed in production, while the Collector receiver is the supported configuration. That is the first design decision you can take for granted. Run it as a receiver in a Collector build, not as the legacy standalone agent.

The profiler’s native unwinding is the interesting engineering. Per the README, native C and C++ executables are unwound using .eh_frame data, so no DWARF debug information is required and system libraries can be profiled without frame pointers. Traces run from kernel space through system libraries into high-level language frames, and inline frames in native code are supported. For interpreted runtimes such as Python or the JVM, dedicated interpreter unwinders read the runtime’s in-memory structures to reconstruct managed frames, again without agents or libraries injected into the process and without restarting interpreters.

Contrast this with the older language-specific continuous profilers. A Java async-profiler agent, a Go pprof HTTP endpoint and a Python sampler each need to be present in every service and each emits its own dialect. A whole-system eBPF agent gives one DaemonSet per node and a uniform stack format, at the price of kernel requirements and privileged access.

What the profiler needs from the host

The kernel floor is a moving target. The README notes the last commit supporting kernel 5.4 and says later changes may require 5.10 or greater, and that it follows the lowest kernel in actively maintained major distributions. A no-kernel-version-check option exists for kernels with backported eBPF features. In practice, check your node images before rollout, particularly long-lived on-prem fleets and some managed-Kubernetes node pools.

Privilege is the second requirement. The sample Collector configuration states the receiver needs elevated access, such as root or capabilities like CAP_BPF, and in containers it needs host PID access and privileged mode or an equivalent. Grafana’s Pyroscope documentation for this integration lists the same requirements: Linux on amd64 or arm64, privileged host access, host PID namespace, and host mounts for /proc, /sys/kernel and /lib/modules, deployed as a DaemonSet. Security teams should review this the same way they review runtime threat-detection agents. If you already run one, the comparison in Falco versus Tetragon versus Tracee for eBPF runtime security covers the same privilege and kernel-compatibility questions.

Sampling rate and overhead: what is actually published

The README states: “1% CPU and 250MB memory are our upper limits in testing”, and says the agent typically stays well below these. That is a project claim from its own testing, not an independent benchmark, and the announcement gives no overhead figures at all. Treat 1% CPU as a design target to verify in your environment.

The sampling frequency defaults differ by source. The example configuration in the repository lists samples_per_second: 20 as the default, while Grafana’s documentation sets samples_per_second: 97 in its example. Both are legitimate; 97 is a common prime-ish choice in profiling because it avoids lockstep with periodic activity at round frequencies such as 100 Hz (this is general profiling practice, not a claim from either document). The cost scales roughly linearly with frequency and with the number of runnable threads, so a fleet-wide rollout should start at the default and move up only for specific nodes.

A worked estimate shows why frequency matters for storage rather than CPU. At 20 samples per second per CPU, a 32-core node that is fully busy produces 640 stack samples per second, or about 55 million per day per node. Without stack deduplication that would be unmanageable, which is exactly the problem the OTLP data model is built to solve.

Probabilistic sampling and the receiver’s knobs

The sample configuration also exposes probabilistic_interval (default 1m) and probabilistic_threshold (default 100), which govern a probabilistic mode that reduces profiling load by only profiling a share of processes or time. The documented option list includes reporter_interval (5s), reporter_jitter (0.2), frame_cache_size (16384), send_idle_frames, send_error_frames, filter_min_process_age, per-interpreter switches and include_env_vars. I could not verify the exact semantics of the probabilistic threshold from the configuration file alone, so check the project’s configuration documentation before depending on it for cost control.

The reporter_jitter setting deserves a note. If every node in a 500-node cluster flushes at the same instant, the gateway sees a synchronized burst. Jitter spreads flushes, which is the same reasoning behind jittered scrape intervals in Prometheus. It is cheap insurance.

The OTLP Profiles Data Model, Field by Field

The OTLP profiles format is a dictionary-encoded tree. Instead of repeating strings, functions and stacks inside every profile, one ProfilesDictionary holds shared lookup tables, and samples refer to them by integer index. The result is a smaller wire size and a model where identical stacks are stored exactly once.

OTLP profiles data model showing ProfilesData, dictionary tables, samples, stacks and links

Figure 2: How an OTLP profiles message points from samples into shared dictionary tables.

Everything below comes from the profiles.proto file in the opentelemetry-proto repository under profiles/v1development. The package name itself is a status signal: it is development-track, and the file marks messages as Alpha with a “since” version for each field. Field numbers and names have moved between releases, which is why version pinning matters later.

ProfilesData and the shared dictionary

ProfilesData holds resource_profiles (field 1) and, since proto release v1.7.0, a single dictionary (field 2) shared across the whole message. This is the biggest departure from the earlier drafts, where each Profile carried its own tables. A shared dictionary lets one export request carry profiles from many scopes and resources while storing a given function name once.

ProfilesDictionary has seven tables, each referenced by index with index zero required to hold the zero value: mapping_table, location_table, function_table, link_table, string_table (index zero must be the empty string), attribute_table and stack_table. The stack_table arrived in v1.8.0. The spec says duplicate and orphaned items should not appear, which pushes dedupe work onto the producer. A sloppy exporter that emits duplicate stacks defeats the compression story.

The attribute table holds KeyValueAndUnit entries, each with a string-table key index, an AnyValue, and a unit string index (zero means implicit, and UCUM is the recommended unit format). Attributes in the dictionary may repeat keys, but any list of attribute_indices that references them from a Profile, Sample, Mapping or Location must keep keys unique.

Profile, Sample and Stack

ResourceProfiles and ScopeProfiles mirror the other signals: a Resource, an InstrumentationScope and a schema_url. Below them, a Profile carries sample_type (the type and unit of each entry in Sample.values), the list of samples, time_unix_nano, duration_nano, period_type and period. The proto notes the time, duration and period fields are informational and do not affect interpretation. A 16-byte profile_id is optional, and an all-zero value is invalid.

Two fields exist for interoperability. original_payload_format and original_payload, which must be set together, let a producer attach the source blob, for example a pprof file, to the converted profile. That is how a lossless round trip is possible: convert to OTLP, and if needed recover the original.

A Sample has an identity of {stack_index, attribute_indices, link_index}, plus per-observation data in values and timestamps_unix_nano. The proto requires at least one of values or timestamps, and equal length if both are set. This identity rule is the heart of the design. Two observations of the same stack with the same attributes and the same trace link belong to one sample entry whose values and timestamps grow, rather than two entries.

A Stack is just a list of location_indices, leaf frame first. A Location has a mapping_index (zero means unknown), an address, a list of Line entries (more than one means inlined frames), and attributes. A Line points to a Function and holds a one-based line and column number, where zero means unset. A Function has name, system name and file name indices plus a start line, and at least one of the name or file fields must be present. A Mapping describes a memory range, file offset and filename.

Worked example: why stacks dedupe

Suppose a service serves requests through the call path main -> handler -> encodeJSON -> memmove. The eBPF agent observes this stack 4,000 times in a minute, split across 40 different traced requests. Under the old pprof style, labels such as a trace ID would be attached per sample, so those 40 request groups make 40 distinct samples, each repeating a four-location stack list. In OTLP, the four locations are stored once in location_table, the stack once in stack_table, and each sample is a small record with a stack_index, a link_index and a compact value list. The savings come from structure rather than clever compression, and the announcement quantifies only one piece: the string dictionary yields “40% smaller wire size”. That figure is the project’s, and it depends on workload.

The alpha announcement also lists aggregated data plus timestamped event data, such as individual off-CPU events. Aggregated samples answer “what is hot”, while timestamped samples answer “what happened at 14:02:07.331”, which is the foundation of timeline views and trace joins.

The pprof mapping and the lossless translator

Mapping pprof to OTLP is mostly structural. pprof’s Sample.location_id list becomes a Stack, pprof’s separate ID spaces become dictionary indices, pprof’s string_table becomes the shared string table, and pprof’s Sample.label entries map naturally onto attributes (my reading of the two schemas; confirm against the translator source). The alpha post says the project built a native translator that round-trips with pprof losslessly, and the original_payload field is the escape hatch for anything that does not map cleanly.

Differences worth knowing. pprof uses non-zero IDs with zero meaning absent. OTLP uses indices with zero reserved as the null element. pprof has no first-class concept of a resource, scope or trace link, so those come from the surrounding OTLP envelope. And pprof’s period and sample_type carry over to Profile.period_type, period and sample_type.

Two consequences for migration. First, existing Go services that already expose /debug/pprof do not need new SDKs: a Collector pprof receiver, listed in the alpha announcement, ingests pprof files into the profiles pipeline. Second, you can keep pprof tooling (go tool pprof, flamegraph generators) in your workflow, because the data survives a round trip.

Conformance: profcheck

The announcement lists a conformance checker called profcheck for validating exported profiles against the spec and semantic conventions. Given how many invariants the proto states (zero entries, no orphans, equal-length value arrays, unique keys per reference list), run it in CI against any custom exporter. I have not run it for this article, so treat its exact interface as something to read from the repository.

Joining Profiles to Traces and Running It in Production

Correlation is the feature that justifies a unified signal. The proto defines a Link table holding a 16-byte trace_id and an 8-byte span_id, and each Sample can carry a link_index (zero means no link). A backend that stores both traces and profiles can then answer “show me the code that ran inside this slow span”.

Sequence showing how an eBPF profiler agent and an SDK share process context so profile samples link to trace and span IDs

Figure 3: Trace-to-profile correlation needs the profiler and the SDK to agree on per-thread context.

Why linking from eBPF is hard

The trace ID lives inside your application process, in a context object that the SDK created. The eBPF agent lives outside it and sees only memory and stacks. For the agent to stamp a sample with the current span, something in the process must publish the active trace and span ID somewhere the agent can read, per thread, at the moment of the sample.

The alpha announcement is explicit that this is future work: “process and thread context sharing, via OTEPs, to link SDK runtime information with eBPF agents” is on the planned list, alongside sharing information between eBPF-based agents (it names OBI, the OpenTelemetry eBPF Instrumentation, and the profiling agent) and standardizing the symbolization API and storage format. The receiver configuration contains an obi_process_ctx option, defaulting to false, which suggests early plumbing; I have not verified what it does, so do not rely on it.

The practical reading in October 2026: the data model supports links, and in-process profilers can attach them today if they integrate with the tracing SDK, but fully automatic span-level linking from the whole-system eBPF agent is still being designed. If span-level flamegraphs are your goal, plan for service-level correlation first. That means joining on shared resource attributes (service, pod, host, time window) rather than on span IDs. It is weaker but robust, and it answers the common question “what was this service doing during the incident window”.

The Collector pieces

Per the announcement, the Collector needs v0.148.0 or newer for profiles support. The pieces listed are the pprof receiver, the k8sattributesprocessor for pod and namespace metadata, OTTL support for transforming or filtering profiles, and an OTLP Resource model update that the Collector supports transparently for profiles. A later Datadog open-source newsletter notes that in Collector v0.149.0 core components such as pdata/pprofile, connector/forward, exporter/debug, exporter/otlp_grpc, exporter/otlp_http and the nop receiver and exporter had their profiles support promoted from development to alpha.

The feature gate is the other gotcha. Both the repository README and Grafana’s documentation require starting the Collector with the service.profilesSupport feature gate (written --feature-gates=+service.profilesSupport). Without it, a profiles pipeline fails validation. Remember that gate names and component names are explicitly expected to change during alpha.

A runnable Collector configuration

This is a condensed version of the repository’s example, with an OTLP exporter and a Kubernetes attributes processor added. Option names come from the example config; verify against the release you pin.

# otel-profiling.yaml  (requires --feature-gates=+service.profilesSupport)
receivers:
  profiling:
    samples_per_second: 20        # repository default
    reporter_interval: 5s
    reporter_jitter: 0.2
    frame_cache_size: 16384

processors:
  k8sattributes:
    extract:
      metadata: [k8s.namespace.name, k8s.pod.name, k8s.node.name]
  batch: {}

exporters:
  otlp_grpc/backend:
    endpoint: profiles-gateway.observability:4317
    tls:
      insecure: true            # lab only, use mTLS in real clusters
  debug:
    verbosity: basic

service:
  pipelines:
    profiles:
      receivers: [profiling]
      processors: [k8sattributes, batch]
      exporters: [otlp_grpc/backend, debug]

Launching it on a host looks like this, again from the README:

sudo ./otelcol-ebpf-profiler \
  --feature-gates=+service.profilesSupport \
  --config otel-profiling.yaml

The batch processor in a profiles pipeline is an assumption on my part. The alpha announcement does not list which general processors support the profiles signal, so remove it if validation fails and run the debug exporter first to confirm data flows.

A Kubernetes DaemonSet skeleton

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: otel-ebpf-profiler
  namespace: observability
spec:
  selector:
    matchLabels: { app: otel-ebpf-profiler }
  template:
    metadata:
      labels: { app: otel-ebpf-profiler }
    spec:
      hostPID: true
      containers:
        - name: profiler
          image: example.registry/otelcol-ebpf-profiler:PINNED_TAG  # build or pin yourself
          args:
            - --feature-gates=+service.profilesSupport
            - --config=/etc/otel/otel-profiling.yaml
          securityContext:
            privileged: true
          volumeMounts:
            - { name: proc, mountPath: /proc, readOnly: true }
            - { name: kernel, mountPath: /sys/kernel, readOnly: true }
            - { name: modules, mountPath: /lib/modules, readOnly: true }
            - { name: cfg, mountPath: /etc/otel }
      volumes:
        - { name: proc, hostPath: { path: /proc } }
        - { name: kernel, hostPath: { path: /sys/kernel } }
        - { name: modules, hostPath: { path: /lib/modules } }
        - { name: cfg, configMap: { name: otel-profiling-config } }

The image name above is a placeholder; I did not verify a published official image tag, so build the distribution yourself or pin one from your registry. The mounts match Grafana’s documented requirements. If you use k8sattributes, add RBAC permissions for the service account, as the Pyroscope documentation also notes.

Backends: the real constraint

The ClickHouse engineering write-up on the signal says plainly that production-ready backends have not yet emerged, that ClickStack does not yet have native profile visualization, and recommends treating 2026 as an evaluation window. The alpha post points experimenters to Elastic’s devfiler desktop application, which reimplements a backend for experimentation and is explicitly not production.

Grafana’s Pyroscope documentation shows the path for self-hosters: the profiler (as a Collector) exports OTLP gRPC to Pyroscope on port 4040, and by default an ingestion relabeling rule must map process.executable.name to service_name. The same page calls the feature suitable for development and testing, warns of breaking changes that need careful version management, and says function names may not resolve in flamegraphs for some programs because symbol resolution is still being improved.

I could not verify current OTLP profiles ingestion claims for Parca, Polar Signals, Datadog or Grafana Cloud beyond that, so check each vendor’s release notes before committing. For positioning, my own opinion: Pyroscope and Parca are the open-source continuous profilers whose storage and query layers matter most, and both historically speak pprof-style protocols, which makes the pprof compatibility story in OTLP the practical bridge during the transition.

Symbolization, Topology and Cost: Designing the Pipeline

Symbolization is turning an address like 0x7f3a1c2b44d0 in a mapped file into encodeJSON at encode.go:212. It is the least glamorous and most failure-prone stage of any profiling pipeline, and it decides whether your flamegraphs are readable or a wall of hex.

Deployment topology for OpenTelemetry profiling with node agents, a gateway collector, a symbolization service and storage

Figure 4: A reference topology separating collection, enrichment, symbolization and storage.

Where symbols come from

For interpreted and managed runtimes, the frame is already a name: the Python unwinder reads function and file from interpreter structures, and the JVM unwinder reads method identity from the runtime. Native code is harder. The binary on the node may be stripped, and the debug symbols live in a build artifact, a debuginfod server or a vendor package repository.

The alpha announcement says Go executables now get automatic on-target symbolization, which works because Go binaries carry their own symbol tables. For other native code, the eBPF repository README only says it can profile native code without host debug symbols, which refers to unwinding rather than naming, and the announcement lists standardizing the symbolization API and storage format, with a reference implementation, as planned work. Today, then, native symbolization is backend-specific. Elastic’s devfiler handles it in the lab; Grafana’s documentation concedes unresolved function names.

This leads to the design principle in Figure 4: keep the agent cheap and push symbolization off the node. The agent ships addresses plus a mapping identity (build ID or file ID); a service with access to your debug-symbol store resolves them later. The advantages are that symbols never have to ship to production hosts, you can re-symbolize old data after uploading missing debug files, and symbol caches are shared across nodes. The disadvantage is a new stateful service and a symbol-upload step in CI. This architecture is how the older proprietary pipelines work in general terms; it is not specified by the OTel proto, which only gives you Mapping, Location and Function tables to carry either raw or resolved data.

Topology choices

Figure 4 uses two collection paths feeding one gateway Collector. Node-level DaemonSets run the eBPF profiler and ship OTLP to a gateway. Go services that already expose pprof can be scraped or pushed through a Collector with the pprof receiver. The gateway applies k8sattributes and OTTL transforms, forwards to the symbolizer and storage, and the UI links profiles to traces by shared attributes.

Why a gateway? Three reasons. First, enrichment with k8sattributes is cheaper when performed on one tier with a cached API-server watch than inside every privileged node agent. Second, the node agent’s attack surface stays minimal. Third, you get one place to apply OTTL to drop noise, for instance filtering profiles from system namespaces or redacting environment-derived attributes if you enabled include_env_vars. That option is dangerous by design: environment variables routinely contain credentials, so leave it empty unless you filter aggressively.

The same sizing arguments apply here as for logs and traces. Compare the tradeoffs among agents, gateways and routers in OpenTelemetry Collector versus Vector versus Fluent Bit, and consider the transport itself in OTLP versus Prometheus remote write, because profiles inherit OTLP’s gRPC and HTTP behavior, including retry and backpressure semantics.

A cost model you can reproduce

Let me give a back-of-envelope model, flagged as illustrative arithmetic and not a benchmark. Take a 200-node fleet averaging 16 busy cores per node at 20 Hz. That yields 200 x 16 x 20 = 64,000 stack observations per second, or about 5.5 billion per day. Raw, at even 100 bytes each, that would be 550 GB per day.

Now apply structure. Real services spend most of their time in a modest number of distinct stacks. If, say, 20,000 distinct stacks per node account for nearly all samples in a reporting interval, the agent emits 20,000 stack table entries plus a counter per stack, not one entry per observation. Aggregated per five-second interval across a node, the wire size becomes dominated by distinct stacks and attributes, not by observation count. The actual ratio depends on workload cardinality; I cannot give you a verified number, and neither does the project. Measure bytes exported per node per hour in the debug pipeline during a one-week pilot and extrapolate.

Cardinality is the real hazard. Each extra attribute on a sample (a pod UID, a request path, a user ID) multiplies the sample identity space, because identity includes attribute_indices. Put request-scoped data on links to traces, and keep sample attributes to low-cardinality dimensions like service, version and thread name.

Version pinning and schema discipline

The proto’s own history tells you why you must pin. Fields carry “since” annotations from v1.4.0 through v1.10.0: stack_table and duration_nano arrived in v1.8.0, samples, profile_id and original_payload in v1.9.0, and Sample.values and link_index in v1.10.0. Moving data from a v1.4-era producer to a v1.10-era backend is not a no-op, and the Pyroscope page warns that breaking changes need careful version management. Pin one proto version across agent, Collector and backend, upgrade them together in a canary cluster, and keep the debug exporter on during upgrades.

Trade-offs, Gotchas, and What Goes Wrong

Alpha means churn. The signal, the proto package (v1development), the feature gate and several component names are all expected to change. The project says Beta and GA are the next milestones and gives no dates. A pipeline you build today will need maintenance, and any backend you choose must track the same proto version. Budget engineering time for upgrades rather than assuming set-and-forget.

Privileged DaemonSets are a security decision. The receiver needs root or capabilities such as CAP_BPF, host PID and often privileged mode. That is a large trust grant for a telemetry agent. Mitigate with a dedicated namespace, Pod Security admission exceptions scoped to it, image signing, read-only mounts where possible, and a clear rollback switch. Remember too that a sampling profiler sees every process on the node, including other tenants, so multi-tenant clusters need a data-access policy for the profile store.

Kernel and architecture drift. The README documents that the minimum kernel has already moved from 4.19 to 5.4 to 5.10 across commits and may rise again. Heterogeneous fleets with old kernels on some pools will silently have gaps. Track agent start-up errors as a metric, alert on nodes without a running profiler, and test every new node image.

Unwinding is only as good as the runtime support. Alpha release notes list improvements per runtime (Go on-target symbolization, ARM64 for Node.js V8, initial BEAM support, .NET 9 and 10, Ruby fixes), which implies those areas were gaps recently. If your critical services run a runtime outside the verified list, or an unusual JIT configuration, test before trusting a flamegraph. A flamegraph with a large “unknown” frame bucket is a symptom, not a result.

Sampling bias and blind spots. On-CPU sampling shows where CPU time goes, not where requests wait. Latency problems caused by lock contention, I/O waits or network stalls require off-CPU data, which the data model supports through timestamped events but which is more expensive to collect and more complex to interpret. Do not conclude a service is “not slow in code” from an on-CPU profile alone.

Statistical noise at low frequency. A function that consumes 0.5% of CPU across a fleet is visible after aggregating millions of samples, but invisible in a single process over thirty seconds. Continuous profiling’s value comes from aggregation across time and replicas. Compare profiles over comparable windows, and be skeptical of single-pod claims.

Cardinality blowups and secrets. As covered above, high-cardinality sample attributes inflate storage, and include_env_vars can leak credentials. Apply OTTL filters at the gateway and review attribute lists the way you review log fields.

Symbolization mismatch. If the binary deployed is not the binary whose debug symbols you stored, the names will be wrong rather than missing, which is worse. Key symbol stores by build ID and verify in CI that every released artifact has an uploaded symbol file.

Treating the OTel signal as a vendor replacement. Mature commercial and open-source continuous profilers have query languages, diffing, retention policies and UI polish that an alpha data format does not provide. OTLP profiles gives you a portable ingest format; it does not give you a profile backend.

Practical Recommendations

For a platform team in October 2026, my recommendation is a staged evaluation rather than an adoption. Opinion, not a vendor claim: the data model is credible and the eBPF profiler is the most advanced piece, but the backend ecosystem is the limiting factor.

Stage one is a lab. Run the Collector-based eBPF profiler on a non-production cluster with the debug exporter, confirm it works on your kernels and runtimes, and measure CPU, memory and export bytes against the project’s 1% CPU and 250 MB ceilings. Stage two is a backend pilot with Pyroscope or another OTLP profiles-capable store on a single non-critical namespace, with the version pinned. Stage three, only after Beta, is a production rollout with symbolization and retention policy.

In parallel, keep your existing profiler. If your services already export pprof, the pprof receiver lets you run both paths and compare, which also validates the translator on your own data. Do not rip out a working Pyroscope or vendor setup on the basis of an alpha announcement.

Adoption checklist:

  • Confirm node kernels meet the profiler’s current minimum, and record the exact versions.
  • Pin one OTLP proto version and Collector version (v0.148.0 or newer) across agents, gateways and backend.
  • Enable the service.profilesSupport feature gate and start with the default sampling rate.
  • Run the debug exporter and profcheck before sending anything to a real backend.
  • Deploy through a DaemonSet in a dedicated, tightly scoped namespace, with documented privilege justification.
  • Leave include_env_vars empty, and keep sample attributes low-cardinality.
  • Decide where symbolization happens and key debug symbols by build ID.
  • Start with service-level correlation (shared resource attributes) and treat span-level links as a later upgrade.
  • Set an overhead budget, alert if the agent exceeds it, and define a kill switch.
  • Re-evaluate at Beta; track the SIG repositories for changes.

Frequently Asked Questions

What is the OpenTelemetry profiling signal?

It is the fourth OpenTelemetry signal alongside traces, metrics and logs. A profile is a collection of samples, typically stack traces with values such as CPU time, showing where code consumes resources. OTLP profiles defines the wire format, with dictionary tables for strings, functions, locations and stacks. As of the March 2026 announcement it is in public alpha, and the SIG says it should not be used for critical production workloads.

Is OpenTelemetry profiling production ready in 2026?

Not yet, by the project’s own statement. The Profiling SIG describes the signal as alpha, says production-ready backends have not yet emerged, and names Beta and GA as future milestones without dates. The eBPF profiler’s Collector receiver is the supported configuration, but standalone binaries are unsupported. Use it for evaluation, pilots and non-critical workloads, with versions pinned and a rollback plan.

How does OTLP profiles relate to pprof?

OTLP profiles is a new format, not pprof wrapped in OTLP, but it is designed for compatibility. The alpha announcement says a native translator converts to and from pprof losslessly, and the format can carry an original payload alongside the converted profile. The Collector also has a pprof receiver, so services that already expose pprof can feed a profiles pipeline without code changes.

How much overhead does the OpenTelemetry eBPF profiler add?

The project’s README states that 1% CPU and 250 MB of memory are its upper limits in its own testing, and that the agent typically stays well below them. The alpha announcement publishes no overhead figures, and I know of no independent benchmark I could verify. Overhead depends on sampling frequency, runnable thread count and runtime mix, so measure it on your own nodes during a pilot.

The data model supports it: a Link holds a trace ID and span ID, and each sample can reference a link. Automatic linking from the whole-system eBPF agent depends on process and thread context sharing between SDKs and the agent, which the SIG lists as planned work. Today, expect service-level correlation through shared resource attributes, and span-level links mainly from in-process profilers that integrate with tracing.

What does the eBPF profiler need to run?

Linux on amd64 or arm64 with a sufficiently recent kernel (the README says later changes may require 5.10 or greater), privileged access or capabilities such as CAP_BPF, and host PID visibility. In Kubernetes that means a DaemonSet with host mounts for /proc, /sys/kernel and /lib/modules. It needs no agents inside profiled processes and no restarts of interpreters or virtual machines.

Further Reading

By Riju — about

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *