ObservabilityXLVI · Tempo DeploymentTempoDeployment
Tempo Receivers
What you'll learn
- Configure OTLP gRPC and HTTP receivers in the Tempo distributor
- Configure Jaeger and Zipkin receivers for legacy clients
- Choose the correct port for each protocol and validate the listener
- Enable TLS on receivers and verify it with openssl
- Diagnose the most common receiver-side failure shapes
Prerequisites
Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13
A team adds OpenTelemetry instrumentation to a payment service
and points the OTLP exporter at tempo:4317. The exporter
reports ExporterExportingFailedPermanent after a few seconds.
The Tempo logs show no traffic. The exporter was sending
correctly-shaped protobuf frames to a port that nobody had bound.
The Tempo binary was running in single-binary mode but the
distributor.receivers block had not been enabled. The exporter
saw a connection, but the connection was on a different process.
Receivers are the front door of Tempo. The shape of that front door (which protocols are accepted, on which ports, with what TLS) determines whether the rest of the system ever sees a span.
What it is
A Tempo receiver is a protocol listener inside the distributor that accepts spans from clients. Tempo currently supports four receiver kinds:
- OTLP — OpenTelemetry Protocol, the current default for new services. Available over gRPC (port 4317) and HTTP/protobuf (port 4318).
- Jaeger — Thrift over UDP (ports 6831/6832) and gRPC (port 14250). Still widely used by services that pre-date the OpenTelemetry SDK migration.
- Zipkin — Thrift and JSON over HTTP (port 9411). Legacy.
- Kafka — not a push receiver in the same sense; spans are
read from a Kafka topic. Discussed in the Kafka ingress
lesson; not configured under
distributor.receivers.
Each receiver is a stanza under distributor.receivers in
tempo.yaml. A receiver can be enabled or disabled independently
of the others.
Why a sysadmin cares
The receiver choice is a fleet decision, not a per-service one. A service sending Zipkin Thrift on UDP cannot be asked to send OTLP/gRPC without code change. The receiver set on Tempo decides which client libraries can send data without an SDK migration.
It is also the boundary in the network. Receivers are the only components that accept unauthenticated traffic from application hosts (unless an auth proxy is in front). Misconfiguration here exposes the cluster to spam, data injection, and resource exhaustion.
How it works
OpenTelemetry SDK Jaeger SDK Zipkin SDK
(OTLP gRPC :4317) (Thrift UDP :6831) (HTTP :9411)
| | |
v v v
+-----------+ +-----------+ +-----------+
| OTLP gRPC | | Jaeger | | Zipkin |
| receiver | | receiver | | receiver |
+-----------+ +-----------+ +-----------+
| | |
+---------------+------------------+------------------------+
v
Tempo distributor
(in-memory trace buffers)
|
v
Ingester ring
The distributor sits in front of the ingester ring. Receivers
deserialize the wire protocol into Tempo’s internal span format
and push the spans onto the ingester. Authentication, if any,
happens at the receiver boundary via the auth_context block.
OTLP
OTLP has two encodings on two ports:
- gRPC. Port 4317. Protobuf over HTTP/2. The default for service meshes and Kubernetes sidecars.
- HTTP. Port 4318. Protobuf over HTTP/1.1. Used by browser exporters and by services that cannot speak gRPC.
Jaeger
Three sub-receivers exist inside the Jaeger stanza:
- Thrift compact over UDP on port 6831. The original Jaeger client default. UDP means spans are dropped under load; not recommended for new services.
- Thrift binary over UDP on port 6832. Same caveat.
- gRPC on port 14250. The migration target for Jaeger SDK users; same wire efficiency as OTLP.
Zipkin
A single HTTP receiver on port 9411 accepts both Thrift and JSON spans. Use only for legacy migration.
Under the hood
How to configure it
OTLP gRPC + HTTP
# /etc/tempo/tempo.yaml — distributor receivers
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
max_recv_msg_size_mib: 16
http:
endpoint: 0.0.0.0:4318
# Optional authentication. Tokens issued by an upstream auth
# service are passed in via gRPC metadata.
auth_context:
extractors:
- name: org-id
from: header
key: X-Scope-OrgID
Severity: CONFIGURATION. Restart the distributor to apply.
The max_recv_msg_size_mib default is 16 MiB. Services emitting
large spans (e.g. spans with thousands of attributes) may hit
resource exhausted. Raise it.
Jaeger
distributor:
receivers:
jaeger:
protocols:
grpc:
endpoint: 0.0.0.0:14250
thrift_compact:
endpoint: 0.0.0.0:6831
thrift_binary:
endpoint: 0.0.0.0:6832
Severity: SERVICE-IMPACT. UDP receivers do not honour
max_recv_msg_size_mib in the same way; tune the kernel UDP
buffer (net.core.rmem_max) for high-volume UDP traffic.
Zipkin
distributor:
receivers:
zipkin:
endpoint: 0.0.0.0:9411
TLS on receivers
Tempo terminates TLS at the receiver for the OTLP HTTP and gRPC receivers. The Jaeger gRPC receiver accepts TLS the same way.
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
tls:
cert_file: /etc/tempo/tls/tempo.crt
key_file: /etc/tempo/tls/tempo.key
client_ca_file: /etc/tempo/tls/ca.crt # mTLS
Severity: CONFIGURATION. The cert and key paths must be
readable by the Tempo process; permissions of 0640 owned by
tempo:tempo are typical.
How to validate it
Severity: READ-ONLY.
# Substitute your own value before running:
TRACE_ID=4bf92f3577b34da6a3ce929d0e0e4736
# 1. Confirm the listener is bound (OTLP gRPC)
ss -tlnp | grep -E '4317|4318|14250|9411|6831|6832'
# 2. Confirm the Tempo distributor is registered
curl -s http://tempo:3200/ready
# 3. Send a synthetic OTLP gRPC span with otel-cli (or any SDK)
otel-cli span export \
--endpoint tempo:4317 \
--service-name validate-receiver \
--name "synthetic check" \
--kind server
# 4. Confirm the trace by ID was accepted
curl -s "http://tempo:3200/api/traces/$TRACE_ID" \
| jq -r '.resourceSpans[0].scopeSpans[0].spans[0].name'
Real output:
$ ss -tlnp | grep 4317
LISTEN 0 128 0.0.0.0:4317 0.0.0.0:* users:(("tempo",pid=8421,fd=12))
$ curl -s http://tempo:3200/ready
ready
$ otel-cli span export --endpoint tempo:4317 --service-name validate-receiver --name "synthetic check"
trace_id=4bf92f3577b34da6a3ce929d0e0e4736
$ curl -s http://tempo:3200/api/traces/4bf92f3577b34da6a3ce929d0e0e4736 \
| jq -r '.resourceSpans[0].scopeSpans[0].spans[0].name'
"synthetic check"
# Validate the TLS handshake with openssl
openssl s_client -connect tempo:4317 -servername tempo \
-CAfile /etc/tempo/tls/ca.crt < /dev/null 2>&1 \
| grep -E 'subject=|issuer=|Verification'
Real output:
subject=CN = tempo.observability.svc
issuer=CN = Internal CA G2
Verification: OK
How it can fail
-
The OTLP exporter sends to port 4317 but the receiver only has HTTP enabled. The connection opens (the OS accepts it), but the gRPC handshake fails with
UNIMPLEMENTED. The exporter retries; metrics showtempo_distributor_dropped_spans_totalrising and thelast_erroris “method not implemented”. Symptom: client log spam; zero spans in storage. -
Jaeger Thrift over UDP cannot keep up at peak. The kernel UDP buffer fills; new spans are silently dropped. Symptom:
dmesgshowsUDP: bad checksumorkernel: ... dropwatchwarnings; trace UI shows sparse data during peak hours but full data during off-peak. -
TLS enabled but client_ca_file path is wrong. Tempo starts but the gRPC handshake fails with
x509: certificate signed by unknown authorityat the client. The Tempo log showstransport: authentication handshake failed. Symptom: all TLS clients rejected; plain-text clients still accepted. -
max_recv_msg_size_mibtoo low for a service with rich attributes. The client seesrpc error: code = ResourceExhausted desc = grpc: received message larger than max. Symptom: a single noisy service drops 100% of spans; everything else is fine. -
Receiver enabled on a querier-only process. In microservices mode, the receiver is configured on the distributor. A copy-paste of the same
tempo.yamlto the querier pod does not break the querier, but the gRPC ports are not bound there. Symptom: clients connecting to the querier pod get connection refused; connections to the distributor pod succeed. -
Auth context extractor typo. A header of
X-Scope-OrgIDis requested but the upstream proxy sendsX-Scope-OrgId(a different case). Symptom: all spans land in the anonymous tenant, mixing production data with every other tenant.
How to troubleshoot it
Order of diagnostics, cheapest first:
- Is the listener bound?
ss -tlnp | grep 4317. If the port is not in the list, the receiver stanza did not parse, or the process is not the distributor. - Did the receiver stanza parse?
journalctl -u tempo | grep -i receiversor the Tempo stdout forunknown receivers key. A misspelling likeotlpvsotlpgrpcsilently ignores the stanza. - Is the connection reaching Tempo?
curl -v http://tempo:4318/v1/traces -d '\{\}' -H 'Content-Type: application/x-protobuf'. A400from Tempo means the receiver is alive but the payload was rejected. - Is the ingester accepting? Look at
tempo_distributor_spans_received_totalminustempo_distributor_dropped_spans_total. A non-zero delta confirms the receiver is forwarding.
Security implications
- Receiver surface. Every enabled receiver is an unauthenticated ingress unless an auth context or upstream proxy is in place. Disable receivers that the fleet does not need.
- UDP receivers. Jaeger Thrift UDP accepts any payload from
any source. An attacker who can reach the receiver can flood
the ingester with synthetic spans and exhaust the
max_traces_per_userbudget. Block UDP at the network layer if not in use. - TLS. OTLP gRPC supports TLS natively. The receiver stanza
must reference a certificate and key. mTLS (
client_ca_file) is appropriate for service-mesh deployments where client identity is enforced.
Performance implications
- gRPC vs HTTP/2. OTLP gRPC uses HTTP/2 with header compression; OTLP HTTP uses HTTP/1.1. gRPC is materially cheaper at high volumes.
- UDP. UDP receivers offload the connection state to the
kernel. At 100k spans/sec, the kernel UDP buffer is the
bottleneck, not Tempo. Raise
net.core.rmem_maxandnet.core.rmem_default. - TLS. TLS adds ~5-10% CPU at typical span sizes. The receiver CPU is rarely the bottleneck; the ingester is.
Production guidance
- Enable OTLP gRPC and HTTP. Disable Jaeger and Zipkin once migration is complete — every active receiver is a load and attack surface.
- Set
max_recv_msg_size_mibto 16 (default) and raise only on per-service evidence; large messages slow the distributor. - Run receivers only on the distributor. A common
misconfiguration is to copy
tempo.yamlto every pod; receivers on the querier are wasted ports.
Verification
You should now be able to answer:
- Which port does OTLP gRPC use by default, and which port does OTLP HTTP use?
- Where in
tempo.yamlare receivers configured? - What is the difference between
protocols.grpcandprotocols.thrift_compactunder the Jaeger stanza? - Which two settings control the TLS handshake for the OTLP receiver?
- What does
ResourceExhaustedmean when an OTLP client sees it from Tempo?
Quiz
Knowledge check · 8 questions
Q1. Which port does OTLP gRPC use by default?
Q2. Where in tempo.yaml is a receiver stanza placed?
Q3. Jaeger Thrift over UDP is recommended for high-volume production services.
Q4. Which of these are valid protocols under the OTLP receiver stanza? (select all that apply)
Q5. Name the tempo.yaml key that bounds the maximum span payload size accepted by the OTLP gRPC receiver.
Q6. A client sees `ResourceExhausted` from the OTLP gRPC receiver. What is the most likely cause?
Q7. A querier pod can also accept OTLP writes if the receiver stanza is in the shared tempo.yaml.
Q8. Which settings control the TLS handshake on the OTLP gRPC receiver? (select all that apply)
Passing score: 75%. Answers are checked in this browser.