API Reference
API Reference
GitHub
Analytics and Monitoring

Prometheus Metrics

Production monitoring with Prometheus

GET /metrics

Opt in with metrics.enabled. Nothing binds when it is off, so a disabled installation refuses the connection rather than answering an empty scrape.

MAILYARD_METRICS_ENABLED=true
MAILYARD_METRICS_ADDR=127.0.0.1:9090
MAILYARD_METRICS_TOKEN=a-long-random-string

A listener of its own

The scrape endpoint binds metrics.addr, not server.addr. It serves /metrics and nothing else, and every role runs it, the worker node included.

metrics.addr defaults to 127.0.0.1:9090 — reachable by a sidecar or a node exporter on the same host, and by nothing else. An address that cannot be bound fails the boot, and one sharing a port with server.addr is refused by config validation.

Widening the bind needs a token

metrics.addr set to 0.0.0.0:9090 puts the endpoint on the network, where a scrape reveals your sending volume, queue depth and failure rates to anyone who asks for it. metrics.token gates it behind Authorization: Bearer <token>, compared in constant time.

Boot warns when the bind reaches past this host and no token gates it. A warning and not a refusal, since a private monitoring network is a legitimate place to skip the token.

Counters

MetricLabelsCounts
mailyard_emails_accepted_totalSends accepted into the queue, from every surface: the API, the console, SMTP submission, campaigns
mailyard_emails_finalized_totalstatusMessages reaching a terminal state — sent, failed or suppressed
mailyard_sandbox_captures_totalMessages captured into a sandbox instead of delivered
mailyard_inbound_received_totalInbound messages stored for a verified domain
mailyard_webhook_deliveries_totalstatusWebhook delivery attempts by outcome

Sandbox captures are counted separately rather than as a label on accepted mail. They never enter the queue, and folding them in would make a CI run that sends ten thousand test messages look like sending volume.

Gauges

These are sampled at scrape time rather than tracked continuously, so they always reflect the moment you asked.

MetricLabelsReports
mailyard_emails_by_statusstatusCurrent email rows per status, across all projects
mailyard_email_partitionsDaily partitions on the emails table
mailyard_email_partitions_ceilingThe count past which concurrent queue claims start failing

mailyard_emails_by_status{status="queued"} is your queue depth, and the one to alert on: sending capacity is fine until it is not, and this is where that shows first.

The partition gauges are published as a pair on purpose. The ceiling is overridable per installation, so an alert written against a literal number is wrong on any install that raised it. Write the ratio instead and it stays correct wherever it is deployed:

mailyard_email_partitions / mailyard_email_partitions_ceiling > 0.8

That one is worth having. Crossing the ceiling does not degrade gradually — concurrent claims begin failing outright with “out of shared memory”, and the fix is a retention window or a higher max_locks_per_transaction, neither of which is instant.

A gauge collector cannot hang your scrape

The partition sample is bounded by a five second timeout, and a collector that errors publishes nothing rather than failing the response.

That matters because /metrics is exactly what an operator reads when the database is in trouble. An unbounded catalog query would hang the one endpoint that could tell them why.

The standard Go runtime and process collectors come along as well, so go_goroutines, go_memstats_* and process_resident_memory_bytes are available without any extra configuration.

Scraping

scrape_configs:
  - job_name: mailyard
    metrics_path: /metrics
    authorization:
      credentials: a-long-random-string
    static_configs:
      - targets: ['mailyard:9090']

The target port is metrics.addr. With the loopback default, Prometheus reaches the process from the same host — a sidecar in the same pod, or an agent on the same machine.

Every node exposes its own numbers — the counters are per process and are not aggregated for you. Scrape each node and sum in Prometheus with sum(rate(...)), which is what you want anyway: a per-node breakdown is how you notice one worker doing nothing.

Worth graphing

  • Accepted against finalized. Divergence means the queue is filling faster than it drains.
  • mailyard_emails_finalized_total{status="failed"} as a share of the total. A step change usually means one SMTP route broke, not that mail generally got worse.
  • Queue depth over time, with an alert on sustained growth rather than on a single spike.
  • Webhook failures. These fail quietly by design and nothing in the console shouts about them.
  • Partition ratio, as above.

For per-project numbers rather than per-process ones, use the dashboard and the analytics API — Prometheus metrics are deliberately installation-wide and carry no project label, because a label with unbounded cardinality is how a metrics database falls over.