plex-exporter

plex-exporter

Docker app from cplieger's Repository

Overview

See what your Plex server is doing in Grafana: sessions, libraries, bandwidth, transcoding. Connects to your Plex Media Server and exposes metrics (active sessions, library sizes, bandwidth, transcoding status) on /metrics for Prometheus to scrape and Grafana to visualize. A ready-made Grafana dashboard ships in the project repository (grafana-dashboard.json). Read-only against Plex, no web UI of its own.

plex-exporter

Image Size Platforms base: Distroless Test coverage Mutation OpenSSF Best Practices OpenSSF Scorecard SBOM

See what your Plex server is doing in Grafana: sessions, libraries, bandwidth, transcoding.

What it does

Connects to your Plex Media Server and exposes metrics (active sessions, library sizes, bandwidth, transcoding status) in a format that Prometheus can scrape and Grafana can visualize.

Key metrics exposed:

  • Library duration, storage, and item counts (movies, episodes, tracks)
  • Active session details (user, device, resolution, stream type)
  • Transcode type detection (video/audio/both) and subtitle handling
  • Session bandwidth and location (LAN/WAN)
  • Host CPU and memory utilization (Plex Pass)
  • Bandwidth transmission totals (Plex Pass)
  • HTTP polling reachability
  • Active transcode session count

Why this design

  • Polling /status/sessions for real-time session tracking: polls the Plex sessions API every 5s, so new sessions appear within seconds; the tracker prunes sessions after 60s of inactivity
  • Single binary: direct dependencies are prometheus/client_golang and a few small helper libraries (see Dependencies); everything else is the standard library
  • Distroless and rootless: runs on gcr.io/distroless/static-debian13 as UID 65532 with no shell or package manager, minimizing attack surface
  • Prometheus-native: exposes a standard /metrics endpoint that works with any Prometheus-compatible scraper and any Grafana dashboard, no custom visualization layer

Limitations

  • Plex Pass features degrade gracefully. CPU/memory utilization and bandwidth statistics require Plex Pass. Without it, those metrics are simply absent; every other metric still works.
  • Library item counts are cached. Episode, track, and item counts are refreshed every 15 minutes to avoid hammering the Plex API. Counts may lag slightly after large library scans.

Quick start

Available from both ghcr.io/cplieger/plex-exporter and docker.io/cplieger/plex-exporter; identical images and tags.

services:
  plex-exporter:
    image: ghcr.io/cplieger/plex-exporter:latest
    container_name: plex-exporter
    restart: unless-stopped

    environment:
      PLEX_URL: "http://plex:32400"  # full URL including scheme and port
      PLEX_TOKEN: "your-plex-token"  # admin token from Plex Web settings

    ports:
      - "9594:9594"

Configuration reference

Environment variables

Variable Description Default Required
PLEX_URL Full URL of your Plex Media Server including scheme and port (e.g. http://192.0.2.100:32400) none Yes
PLEX_TOKEN Plex authentication token for the server administrator. Get it from Plex Web → Settings → XML view → myPlexAccessToken. Also readable from a file: see PLEX_TOKEN_FILE below none Yes
PLEX_TOKEN_FILE Path to a file holding the Plex token (a Docker or Podman secret). When set it takes precedence over PLEX_TOKEN and keeps the token out of the container environment, so it does not appear in docker inspect. One trailing line ending is stripped; a file holding only whitespace is rejected at startup (unset) No
LISTEN_ADDR Address and port for the metrics HTTP server :9594 No
PLEX_CA_CERT_PATH Path to a PEM file with the CA that signed your Plex server's certificate; TLS verification stays on, pinned to that CA. See TLS / certificate setup. (unset) No

TLS / certificate setup

Pick the configuration that matches your Plex server:

Your PLEX_URL looks like What to do
http://plex:32400 (Docker network, LAN, etc.) nothing; TLS isn't in use
https://<hash>.plex.direct:32400 (Plex's official cert) nothing; Let's Encrypt is trusted by default
https://192.0.2.100:32400 or https://plex.local (self-signed / private CA) set PLEX_CA_CERT_PATH to the PEM file of the CA that signed your Plex cert

Ports

Port Description
9594 Prometheus metrics endpoint (/metrics) and health check (/api/health)

Hardened and Kubernetes deployments

The health probe writes a marker file to /tmp/.healthy (see Healthcheck), so a read-only root filesystem needs a writable /tmp. Without one the probe can never pass and the container is reported unhealthy while the exporter itself works. A small in-memory mount is enough; the marker is an empty file.

Docker Compose:

    read_only: true
    tmpfs:
      - "/tmp:size=1m,mode=1777,noexec,nosuid,nodev"

Kubernetes, alongside securityContext.readOnlyRootFilesystem: true:

    volumes:
      - name: tmp
        emptyDir:
          medium: Memory
          sizeLimit: 8Mi
    # in the container spec
    volumeMounts:
      - name: tmp
        mountPath: /tmp

One more thing worth knowing on Kubernetes: the Prometheus Operator adds pod, endpoint and container labels to every scraped series, and the pod value changes on each restart. A panel reading a single gauge over a time range therefore renders one entry per dead pod until the old series ages out.

The shipped dashboard handles this by asking current-state tiles for an instant value rather than a range, so stale pod series cannot appear. If you need those labels gone at ingestion instead, drop them in the ServiceMonitor:

    metricRelabelings:
      - action: labeldrop
        regex: (pod|endpoint|container)

Do this only for a single-replica deployment. Those labels are what distinguishes one replica's series from another's, so dropping all three on a multi-replica ServiceMonitor makes two replicas emit identical label sets, which Prometheus warns against explicitly. With more than one replica, keep pod and aggregate in the query instead.

Metrics reference

HTTP Endpoints

Endpoint Method Description
/metrics GET Prometheus metrics (see below)
/api/health GET Returns {"status":"OK"} when ready, 503 when starting/stopping

Server Metrics

Metric Type Labels Description
plex_server_info Gauge (always 1) server, server_id, version, platform, platform_version, plex_pass Server metadata and Plex Pass status
plex_host_cpu_utilization_ratio Gauge server, server_id Host CPU utilization as a ratio (0.0–1.0). Requires Plex Pass.
plex_host_memory_utilization_ratio Gauge server, server_id Host memory utilization as a ratio (0.0–1.0). Requires Plex Pass.
plex_transmit_bytes_total Counter server, server_id Cumulative bytes transmitted (from Plex bandwidth API). Requires Plex Pass. Resets on container restart; indicative only.
plex_active_transcode_sessions Gauge server, server_id Number of active video transcode sessions (from root endpoint, no Plex Pass needed)
plex_http_reachable Gauge server, server_id HTTP polling reachability: 1 = last refresh succeeded, 0 = failed
plex_session_poll_reachable Gauge server, server_id Session poll reachability: 1 = last /status/sessions poll succeeded, 0 = failed
plex_http_retries_total Counter server, server_id Total HTTP retries performed by the Plex client's retry round-tripper across all requests
plex_exporter_errors_total Counter server, server_id, type Exporter error count by type. Types: refresh, sessions_fetch, metadata_fetch, invalid_rating_key, metrics_server, library_items.

Library Metrics

Metric Type Labels Description
plex_library_duration_milliseconds Gauge server, server_id, library_type, library, library_id Total duration of all items in the library (ms)
plex_library_storage_bytes Gauge server, server_id, library_type, library, library_id Total storage used by the library (bytes)
plex_library_items Gauge server, server_id, library_type, library, library_id, content_type Number of items in the library. content_type is movies, episodes, tracks, photos, or items. Refreshed every 15 minutes. Absent until the count is read once; a library read as empty reports 0.

Session Metrics

Metric Type Labels Description
plex_plays_active Gauge server, server_id, library, library_id, library_type, media_type, title, child_title, grandchild_title, grandchild_index, stream_type, stream_resolution, stream_file_resolution, device, device_type, user, session, transcode_type, subtitle_action, location, local Currently active play sessions (1 per session). Reported once the exporter has seen the session playing, so a stream that was already paused when the exporter started is absent until it resumes. Use count(plex_plays_active) for total stream count. Removed after 60s of inactivity.
plex_play_seconds_total Counter (same as above) Cumulative play time for the session (seconds)
plex_session_bandwidth_kbps Gauge server, server_id, session, user, location Real-time session bandwidth from the Plex Sessions API (kbps)
plex_session_bitrate_kbps Gauge server, server_id, session, user, location Live stream bitrate per session (kbps). Kept as its own series rather than a label on the play metrics, so adaptive-streaming bitrate changes cannot inflate label cardinality.

Session Label Reference

Label Values Description
stream_type directplay, copy, transcode How the stream is being delivered
transcode_type none, video, audio, both What is being transcoded
subtitle_action none, burn, copy, transcode How subtitles are handled
location lan, wan Client network location
local true, false Whether the client is on the local network
media_type movie, episode, track, etc. Plex media type

For episodes: title = show name, child_title = season, grandchild_title = episode title, grandchild_index = episode number (track number for music). For movies: title = movie name, others are empty.

Beyond the values above, every user-controlled label value is normalized to a bounded set, so an unexpected Plex response can never explode Prometheus cardinality: a value outside the documented set becomes other and missing data becomes unknown. This covers stream_type, media_type, location, subtitle_action, and the resolution labels. An empty Plex subtitleDecision is reported as subtitle_action="none".

Alerting

plex-exporter exposes Prometheus metrics on /metrics (see Metrics reference) and writes its own diagnostics to its container log. The rules ship as one file per expression language: scrape /metrics and evaluate the six PromQL rules in alerts/promql.yaml with Prometheus or the Mimir ruler, and ship the container's logs to Loki (Grafana Alloy's Docker log discovery does this with no configuration) to evaluate the three LogQL rules in alerts/logql.yaml with Loki's ruler. Load each half into its own ruler: neither ruler parses the other's expressions. Firing alerts deliver through your Alertmanager either way. They cover:

Alert Fires when Severity
PlexExporterTargetDown no successful scrape of the exporter for 15m, so the scrape itself is failing warning
PlexExporterTargetAbsent the exporter has no up series at all for 15m, so the target has left service discovery warning
PlexAPIUnreachable the authenticated Plex API poll reports plex_http_reachable=0 for 10m (often a revoked or invalid PLEX_TOKEN) warning
PlexSessionPollFailing the /status/sessions poll reports plex_session_poll_reachable=0 for 10m while the rest of the API answers, so every session metric is absent or stale warning
PlexExporterCollectionErrors the plex_exporter_errors_total counter keeps rising for some type over 30m warning
PlexLibraryItemsCollapsed a library's item count drops more than 50% versus its level ~1-2h earlier and stays down for 30m, a drop to exactly zero included warning
PlexExporterFatalError the exporter logs an ERROR: a rejected config or token, a bind failure, a metrics-server failure, or a recovered panic warning
PlexExporterSessionMapFull the session tracker is at its cap and drops new Plex sessions, so the session metrics undercount warning
PlexExporterRefreshIncomplete a refresh cycle runs out of time before the Plex Pass gauges are read, so they keep serving values from an earlier cycle warning

Thresholds, the for: windows, and the severity labels are starting points; add your scrape job label to the selectors if you run more than one instance, adjust the container selector to whatever your log collector sets, and route by whatever labels your Alertmanager uses.

PlexExporterTargetDown and PlexExporterTargetAbsent are the two rules that must carry a job matcher, because they ask whether this exporter is visible at all. Set both to whatever your scrape config calls the exporter, and keep the matcher exact rather than a regex: a regex asks whether any matching target is up, so one healthy replica masks a failed one, and it leaves the absent() result with no label to route on.

Every metric rule reads a series the exporter publishes, so all of them go quiet together when it stops being scraped, which is what those two rules exist to catch. The three log rules are the ones that still fire when there is nothing left to scrape: a configuration the exporter refuses outright reaches PlexExporterFatalError before any series exists.

Healthcheck

The image ships a HEALTHCHECK (the CLI probe /plex-exporter health) that verifies the HTTP server is listening; /api/health serves the same status over HTTP. The container exits (and Docker restarts it) only on a non-recoverable startup error: a bad token or other 4xx (except 408 and 429), the wrong server (404), a TLS/certificate misconfiguration, or a metrics-server start failure. A transient startup failure (DNS, dial, timeout, a 408 or 429, or a 5xx from a Plex that is still starting up) instead brings the exporter up degraded but healthy: it binds /metrics, reports plex_http_reachable=0, and recovers automatically once Plex is reachable again.

Security

Connects outbound to Plex only. The /metrics endpoint serves read-only Prometheus data (standard for internal exporters). PLEX_TOKEN is never logged or exposed in metrics.

The Plex client sends the token in a request header, refuses redirects, restricts requests to the configured server, bounds response reads, and caps each request at 30s. TLS verification always stays on; PLEX_CA_CERT_PATH pins a private CA instead of disabling it. Rating keys are validated as integers before URL construction, and the metrics server sets explicit read, write, and header timeouts. The image runs as a non-root user on a distroless base with no shell or package manager.

Static analysis and vulnerability scans run in CI on every change; current results are on the repository's Security tab. One accepted finding: semgrep reports two informational matches, both reviewed as false positives.

Dependencies

All dependencies are updated automatically via Renovate and pinned by digest or version for reproducibility.

Dependency Source
golang Go
gcr.io/distroless/static Distroless
github.com/prometheus/client_golang GitHub
github.com/prometheus/client_model GitHub
github.com/cplieger/plexapi/v2 GitHub
github.com/cplieger/webhttp/v3 GitHub
github.com/cplieger/health GitHub
github.com/cplieger/envx/v2 GitHub
github.com/cplieger/slogx GitHub
github.com/cplieger/runesafe/v2 GitHub
golang.org/x/sync golang.org/x
pgregory.net/rapid pkg.go.dev

Credits

This is an original tool building on the Grafana Hackathon 2022 prometheus-plex-exporter lineage: the @jsclayton post-hackathon fork and the actively maintained @timothystewart6 fork. It also uses the Plex Media Server API and prometheus/client_golang.

Contributing

Issues and pull requests are welcome. Please open an issue first for larger changes so the approach can be discussed before implementation.

Disclaimer

This project is built with care and follows security best practices, but it is intended for personal / self-hosted use. No guarantees of fitness for production environments. Use at your own risk.

This project was built with AI-assisted tooling using Claude, GPT, and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.

License

GPL-3.0-or-later. See LICENSE.

Install plex-exporter on Unraid in a few clicks.

Find plex-exporter in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.

Open the Apps tab on your Unraid server Search Community Apps for plex-exporter Review the template variables and paths Click Install

Requirements

Needs a Prometheus-compatible scraper (Prometheus, Grafana Alloy, VictoriaMetrics) pointed at port 9594 and Grafana to show the dashboard; neither is included.
Plex URL: the full URL of your Plex server including scheme and port, for example http://192.0.2.100:32400.
Plex Token: the server administrator's token, from Plex Web: Settings, then the XML view of any item, then myPlexAccessToken.
Host CPU, memory and bandwidth metrics require Plex Pass; everything else works without it.
The metrics endpoint has no authentication: keep port 9594 on your LAN.

Download Statistics

20,305
Total Downloads

Related apps

Details

Repository
cplieger/plex-exporter:latest
Last Updated2026-09-14
First Seen2026-09-17

Runtime arguments

Web UI
http://[IP]:[PORT:9594]/metrics
Network
bridge
Privileged
false

Template configuration

Plex URLVariable

Full URL of your Plex Media Server including scheme and port, e.g. http://192.0.2.100:32400

Target
PLEX_URL
Plex TokenVariable

Plex authentication token of the server administrator (Plex Web: Settings, XML view, myPlexAccessToken)

Target
PLEX_TOKEN
Metrics portPorttcp

Prometheus metrics (/metrics) and health (/api/health)

Target
9594
Default
9594
Value
9594
Plex CA certificate pathVariable

Only for an https Plex URL with a self-signed or private-CA certificate: in-container path of the CA PEM file (add a matching path mapping)

Target
PLEX_CA_CERT_PATH