Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for OpenTelemetry By James Joyner IV · · 9 min read

OpenTelemetry Error Guide: 'error reading from server: EOF' — Fix OTLP Dropped Streams

Quick answer

Fix 'rpc error: code = Unavailable desc = error reading from server: EOF': the server or proxy closed the OTLP stream. Tune keepalive.

  • #opentelemetry
  • #observability
  • #troubleshooting
  • #errors
Free toolkit

Fixing errors like this? Get 500 free DevOps AI prompts

500 copy-paste AI prompts for the stack you actually run — one PDF, free.

Overview

This error appears when the server or an intermediary closes the OTLP connection while the exporter is still reading the response. The client reaches end-of-file where it expected more data, and gRPC reports it as Unavailable:

rpc error: code = Unavailable desc = error reading from server: EOF

A closely related transport variant reads:

rpc error: code = Unavailable desc = error reading from server: read tcp 10.0.4.12:52788->10.0.7.9:4317: read: connection reset by peer

An unexpected EOF means the far end hung up mid-stream — an idle timeout fired, the connection was recycled, a message exceeded a limit and the server closed the socket, or a load balancer dropped the backend. Because gRPC classifies it as Unavailable, it is retryable: the in-flight batch is retried if a retry queue is configured, otherwise dropped.

Symptoms

  • error reading from server: EOF appears intermittently, often after idle periods or during backend rollouts.
  • Errors cluster on long-lived connections that have been open for minutes.
  • A load balancer, proxy, or service-mesh sidecar sits between the exporter and the Collector.
  • Large batches fail with EOF while small ones succeed (server closing on size).
  • otelcol_exporter_send_failed_spans blips then recovers as retries land on a healthy connection.

Common Root Causes

  • Idle-connection timeout — an LB/NAT/proxy silently closes an idle keepalive connection; the next export reads EOF on a dead socket.
  • max_connection_age recycle — the server deliberately closes long-lived HTTP/2 connections and the client sees EOF on the in-flight stream.
  • Message too large closing the connection — a batch over the server’s max_recv_msg_size can cause the server to close the socket instead of returning a clean ResourceExhausted.
  • Backend/gateway rollout — a Collector replica terminates mid-request and the stream ends in EOF.
  • Load balancer draining — the LB removes a backend from rotation and cuts the connection.
  • Mismatched keepalive — the client never pings, so a proxy considers the connection idle and closes it between exports.

Diagnostic Workflow

Determine whether EOF follows idle gaps (keepalive/idle-timeout) or large batches (message-size), and what sits in the path:

env | grep OTEL_EXPORTER_OTLP
journalctl -u otelcol-contrib --since '15 min ago' | grep -i 'EOF\|reading from server\|too large\|keepalive'
# Is a mesh sidecar in the pod?
kubectl get pod -l app=my-app -o jsonpath='{.items[0].spec.containers[*].name}'

Keep the connection warm with client keepalive and ensure a retry queue absorbs the occasional recycle:

exporters:
  otlp:
    endpoint: otel-gateway:4317
    tls:
      insecure: false
    keepalive:
      time: 30s               # ping every 30s so idle proxies don't close us
      timeout: 10s
      permit_without_stream: true
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_elapsed_time: 300s
    sending_queue:
      enabled: true
      num_consumers: 4
      queue_size: 5000

On the receiving Collector, permit those pings, recycle connections gracefully, and raise the accepted message size so large batches aren’t closed:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
        max_recv_msg_size_mib: 16
        keepalive:
          server_parameters:
            max_connection_age: 300s
            max_connection_age_grace: 30s
          enforcement_policy:
            min_time: 20s          # must be <= client keepalive time
            permit_without_stream: true

If the EOF only appears on large exports, cap batch size well under the server limit:

processors:
  batch:
    send_batch_size: 512
    send_batch_max_size: 2048   # keep well under max_recv_msg_size

Then validate and confirm the receiver bound its port:

otelcol-contrib validate --config /etc/otelcol-contrib/config.yaml

Example Root Cause Analysis

Application pods exported traces through an L7 load balancer to a gateway Collector. Traffic was bursty: quiet for a minute, then a spike. During the quiet windows the exporter held an idle HTTP/2 connection open but never pinged it, so the load balancer’s 60-second idle timeout closed the backend leg. The next export after each lull read rpc error: code = Unavailable desc = error reading from server: EOF, and because retry_on_failure had been left disabled, that first batch after every idle window was dropped.

The fix had two parts. First, the exporter gained keepalive.time: 30s with permit_without_stream: true so the connection was pinged during idle windows and the LB no longer considered it stale. Second, retry_on_failure and a sending_queue were re-enabled so the rare EOF from a genuine connection recycle became a resumed export. After the change, the post-idle EOFs disappeared and dropped spans went to zero.

Prevention Best Practices

  • Enable client keepalive (time ≥ the server’s enforcement_policy.min_time) so idle proxies and LBs don’t close the connection between bursts.
  • Always keep retry_on_failure and a bounded sending_queue on so a mid-stream EOF is retried, not dropped.
  • Set the server’s max_connection_age with a grace window so long-lived connections are recycled cleanly instead of abruptly.
  • Align proxy/LB idle timeouts with the OTLP route and keep them longer than your keepalive interval.
  • Cap send_batch_max_size under the receiver’s max_recv_msg_size_mib so oversized batches don’t cause the server to close the socket.
  • Alert only on sustained EOF/Unavailable; occasional recycles are normal and should be absorbed by retries.

Quick Command Reference

# Find EOF / mid-stream closes in Collector logs
journalctl -u otelcol-contrib -f | grep -i 'EOF\|reading from server'

# Confirm the gateway is up and speaking gRPC
grpcurl -plaintext otel-gateway:4317 list

# Validate keepalive/message-size config after editing
otelcol-contrib validate --config /etc/otelcol-contrib/config.yaml

# Watch failures recover as retries land
curl -s http://localhost:8888/metrics | grep -E 'exporter_send_failed|exporter_sent'

Conclusion

error reading from server: EOF means the connection ended before the response did — the server, a proxy, or a load balancer hung up mid-stream, usually from an idle timeout, a connection recycle, or an oversized batch. Keep the link warm with client keepalive, let the server recycle connections gracefully with max_connection_age, cap batch sizes under the receiver limit, and lean on retry_on_failure so a rare mid-stream close resumes instead of dropping. Reserve alerts for sustained EOFs so normal connection churn stays invisible.

Free download · 368-page PDF

Get 500 Battle-Tested DevOps AI Prompts — Free

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.