OpenTelemetry’s OTLP protocol can use either gRPC or HTTP as its transport layer, and the choice significantly impacts performance and how you interact with your telemetry data.

Let’s see OTLP in action with both exporters. Imagine we have a simple Go application generating traces.

First, the gRPC exporter:

import (
	"context"
	"log"
	"time"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
)

func initTracerProvider(ctx context.Context) (*sdktrace.TracerProvider, error) {
	// Configure the gRPC exporter
	exporter, err := otlptracegrpc.New(ctx,
		otlptracegrpc.WithEndpoint("localhost:4317"), // Default OTLP/gRPC port
		otlptracegrpc.WithInsecure(), // For local testing, use WithTLSCredentials for production
	)
	if err != nil {
		return nil, err
	}

	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceNameKey.String("my-grpc-app"),
		)),
	)
	otel.SetTracerProvider(tp)
	return tp, nil
}

func main() {
	ctx := context.Background()
	tp, err := initTracerProvider(ctx)
	if err != nil {
		log.Fatalf("failed to initialize tracer provider: %v", err)
	}
	defer func() {
		if err := tp.Shutdown(ctx); err != nil {
			log.Printf("Error shutting down tracer provider: %v", err)
		}
	}()

	tracer := otel.Tracer("my-tracer")
	ctx, span := tracer.Start(ctx, "my-operation")
	defer span.End()

	time.Sleep(2 * time.Second) // Simulate work
	log.Println("Trace sent via gRPC")
}

And now, the HTTP exporter:

import (
	"context"
	"log"
	"time"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
)

func initTracerProvider(ctx context.Context) (*sdktrace.TracerProvider, error) {
	// Configure the HTTP exporter
	exporter, err := otlptracehttp.New(ctx,
		otlptracehttp.WithEndpoint("localhost:4318"), // Default OTLP/HTTP port
		otlptracehttp.WithInsecure(), // For local testing, use WithTLSCredentials for production
	)
	if err != nil {
		return nil, err
	}

	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceNameKey.String("my-http-app"),
		)),
	)
	otel.SetTracerProvider(tp)
	return tp, nil
}

func main() {
	ctx := context.Background()
	tp, err := initTracerProvider(ctx)
	if err != nil {
		log.Fatalf("failed to initialize tracer provider: %v", err)
	}
	defer func() {
		if err := tp.Shutdown(ctx); err != nil {
			log.Printf("Error shutting down tracer provider: %v", err)
		}
	}()

	tracer := otel.Tracer("my-tracer")
	ctx, span := tracer.Start(ctx, "my-operation")
	defer span.End()

	time.Sleep(2 * time.Second) // Simulate work
	log.Println("Trace sent via HTTP")
}

The core problem OTLP solves is providing a vendor-neutral, standardized way to export telemetry data (traces, metrics, logs) from your applications to a backend for analysis. Before OTLP, you’d often use proprietary exporters for each backend (e.g., Jaeger exporter, Prometheus exporter). OTLP abstracts this, allowing you to configure your application once and switch backends by changing the collector or backend configuration, not your application code.

Internally, OTLP serializes telemetry data into a Protocol Buffers (protobuf) format. This protobuf message structure is defined by the OpenTelemetry specification. The gRPC exporter then packages these protobuf messages into gRPC calls, leveraging HTTP/2 for efficient, multiplexed communication. The HTTP exporter, on the other hand, packages the same protobuf messages into the body of standard HTTP POST requests, typically using JSON encoding for the protobuf payload within the HTTP body.

The exact levers you control are primarily the Endpoint and security settings. For gRPC, you’ll point to an OTLP receiver listening on a gRPC port (default 4317). For HTTP, you’ll point to an OTLP receiver listening on an HTTP port (default 4318). For secure communication, you’d replace WithInsecure() with WithTLSCredentials() and provide the appropriate client certificates and trust anchors. You can also configure WithTimeout() and WithCompression() for both.

When choosing between gRPC and HTTP for OTLP, the most surprising truth is that gRPC often performs better not because it’s inherently faster, but because it uses HTTP/2’s multiplexing capabilities to send multiple telemetry signals (traces, metrics, logs) over a single TCP connection, reducing connection overhead. HTTP/1.1, which the HTTP exporter might fall back to or use if not configured for HTTP/2, typically requires a new connection per request, leading to higher latency and resource usage, especially under high load.

The next concept you’ll likely encounter is how to configure your OTLP collector or backend to receive and process these signals, and understanding the different processing stages within the collector pipeline, such as the batchprocessor and memorylimiter.

Want structured learning?

Take the full Opentelemetry course →