temporalio/temporal · critical

unable to create metrics handler: %w

Error message

unable to create metrics handler: %w

What it means

During temporal server (fx) dependency injection, ServerOptionsProvider builds a metrics handler from config.Global.Metrics via metrics.MetricsHandlerFromConfig when no custom handler is injected. Any configuration or initialization failure is wrapped as 'unable to create metrics handler', aborting server startup.

Source

Thrown at temporal/fx.go:213

	stopChan := make(chan any)

	// ClientFactoryProvider
	clientFactoryProvider := so.clientFactoryProvider
	if clientFactoryProvider == nil {
		clientFactoryProvider = client.NewFactoryProvider()
	}

	persistenceFactoryProvider := so.persistenceFactoryProvider
	if persistenceFactoryProvider == nil {
		persistenceFactoryProvider = PersistenceFactoryProvider()
	}

	// MetricsHandler
	metricHandler := so.metricHandler
	if metricHandler == nil {
		metricHandler, err = metrics.MetricsHandlerFromConfig(logger, so.config.Global.Metrics)
		if err != nil {
			return serverOptionsProvider{}, fmt.Errorf("unable to create metrics handler: %w", err)
		}
	}

	// EventLoggerProvider backs structured ("wide") events. Select the custom OTEL LoggerProvider
	// if injected, else a no-op provider that discards events. A deployment opts in by injecting a
	// provider via WithCustomEventLoggerProvider.
	eventLoggerProvider := so.eventLoggerProvider
	if eventLoggerProvider == nil {
		eventLoggerProvider = lognoop.NewLoggerProvider()
	}

	// DynamicConfigClient
	dcClient := so.dynamicConfigClient
	if dcClient == nil {
		dcConfig := so.config.DynamicConfigClient
		if dcConfig != nil {
			dcClient, err = dynamicconfig.NewFileBasedClientWithMetrics(dcConfig, logger, stopChan, metricHandler)
			if err != nil {

View on GitHub (pinned to bde624efd1)

Solutions

  1. Read the wrapped inner error and fix the metrics section of your config yaml
  2. Verify the exporter type and bind address (prometheus) are valid and the port is free
  3. Inject a custom handler with temporal.WithMetricHandler (fx option) if you need nonstandard metrics setup
  4. Temporarily remove/comment the metrics block to confirm it is the failing component

Example fix

// before
metrics:
  prometheus:
    bindAddress: "bad-address"
// after
metrics:
  prometheus:
    bindAddress: "0.0.0.0:9090"
Defensive patterns

Strategy: validation

Validate before calling

// pre-validate metrics config before starting the server
var cfg config.Metrics
if err := yaml.Unmarshal(metricsYaml, &cfg); err != nil { log.Fatal(err) }
if cfg.Prometheus != nil {
    if _, _, err := net.SplitHostPort(cfg.Prometheus.BindAddress); err != nil {
        log.Fatalf("invalid prometheus bindAddress: %v", err)
    }
}

Prevention

When it happens

Trigger: Invalid metrics config section (bad prometheus listen address, unknown exporter type, malformed tags/exclusions), or MetricsHandlerFromConfig returning an error for the chosen metrics reporter settings.

Common situations: Typo in config/template yaml (metrics.prometheus.bindAddress invalid), port already in use, unsupported SDK metrics scope settings, missing required fields in the metrics block.

Related errors


AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01). Data as JSON: /api/errors/0878859bb345b9b7. Report an issue: GitHub.