GitHub

What is Certstream?

What it does

Certstream reads certificates from Certificate Transparency logs and streams them to connected clients. This Rust server uses the Certstream message format, so existing clients work unchanged.

Implementation notes

  • Messages are serialized once and shared between subscribers through Arc<PreSerializedMessage>
  • JSON serialization is skipped when no clients are connected
  • Static CT watchers share one issuer certificate cache
  • simd-json is enabled by default
  • The server ships as a single binary with no runtime dependencies

certificate_update

The default and full streams use the certificate_update message type.

Each message carries two timestamps. seen is when this server processed the entry. submission_timestamp is when the CT log accepted the certificate and issued the Signed Certificate Timestamp (SCT), as defined by RFC 6962 §3.1. Both are Unix timestamps in seconds, with millisecond precision.

JSON certificate_update
{
  "message_type": "certificate_update",
  "data": {
    "update_type": "X509LogEntry",
    "leaf_cert": {
      "subject": {
        "CN": "example.com",
        "O": "Example Inc",
        "C": "US",
        "aggregated": "/C=US/CN=example.com/O=Example Inc"
      },
      "issuer": {
        "CN": "R3",
        "O": "Let's Encrypt",
        "aggregated": "/CN=R3/O=Let's Encrypt"
      },
      "serial_number": "048A3F...",
      "not_before": 1703721600,
      "not_after": 1735257600,
      "fingerprint": "AB:CD:EF:01:...",
      "sha1": "AB:CD:EF:01:...",
      "sha256": "AB:CD:EF:01:...",
      "signature_algorithm": "sha256, rsa",
      "is_ca": false,
      "all_domains": [
        "example.com",
        "www.example.com"
      ],
      "extensions": {
        "keyUsage": "Digital Signature, Key Encipherment",
        "extendedKeyUsage": "serverAuth, clientAuth",
        "basicConstraints": "CA:FALSE",
        "subjectAltName": "DNS:example.com, DNS:www.example.com"
      },
      "as_der": "BASE64..."  // full-stream only
    },
    "chain": [...],  // full-stream only
    "cert_index": 1234567890,
    "seen": 1703808000.123,
    "submission_timestamp": 1703721600.456,  // when the CT log accepted this cert (RFC 6962 §3.1)
    "source": {
      "url": "ct.googleapis.com/logs/argon2025",
      "name": "Google Argon 2025"
    }
  }
}

dns_entries

The /domains-only WebSocket endpoint and /sse?stream=domains use a smaller schema.

JSON domains-only stream
{
  "message_type": "dns_entries",
  "data": [
    "example.com",
    "www.example.com",
    "*.example.com"
  ]
}

Unlike the full and lite streams, message_type is "dns_entries" rather than "certificate_update", and data is a JSON array of strings rather than an object with nested fields. Use this stream when you only need domain names.

Stream types

Lite (default)

Certificate metadata, domains, issuer information, and timestamps. Does not include the DER-encoded certificate or the chain.

Full

Everything in the lite stream plus the base64-encoded DER certificate and the full chain.

Domains only

Domain names only. The schema differs from the other two streams: message_type is "dns_entries" and data is an array of strings.

WebSocket, SSE, and HTTP

WebSocket

EndpointDescription
ws://host:8080/Lite stream (no DER / chain)
ws://host:8080/full-streamFull data with DER and chain
ws://host:8080/domains-onlyDomain names only (message_type: "dns_entries")
ws://host:8080/v2Version 2 output (off by default)

Unfiltered WebSocket streams support permessage-deflate (RFC 7692). A client that offers it receives about 5 times less data on the lite stream, with nothing to change in its code: Python websockets, Node ws and browsers offer it by default. The stream is compressed once and shared by every compressing client, so the server's cost does not grow with them. A client starts at the next compressor reset, at most 128 messages after it connects, and after missing messages waits for the next one. Filtered streams, and clients that ask for server_no_context_takeover or a window below 15 bits, are sent uncompressed.

SSE

EndpointDescription
http://host:8080/sseLite (default)
http://host:8080/sse?stream=fullFull
http://host:8080/sse?stream=domainsDomains only
http://host:8080/sse?stream=v2Version 2 output (off by default)

HTTP

EndpointDescription
/healthBasic health check (returns "OK")
/health/deepDetailed health: log status, connections, uptime (JSON)
/metricsPrometheus metrics
/example.jsonExample message

REST API

Disabled by default. Enable with CERTSTREAM_API_ENABLED=true.

EndpointDescription
GET /api/statsServer statistics (uptime, connections, throughput, cache)
GET /api/logsCT log health status (healthy / degraded / unhealthy)
GET /api/cert/{hash}Lookup certificate by SHA256, SHA1, or fingerprint

Server-side filters

Every WebSocket and SSE endpoint accepts domain and issuer query parameters. Terms inside one parameter are alternatives; the two parameters narrow together.

wscat -c 'ws://localhost:8080/?domain=example.com,example.org'
curl -N 'http://localhost:8080/sse?stream=lite&domain=example.com&issuer=let%27s+encrypt'
ParameterMatches
domainThe name itself and any subdomain, at label boundaries. example.com matches example.com, www.example.com and *.example.com; it does not match notexample.com.
issuerCase-insensitive substring of the issuer CN or O, so both R10 and let's encrypt work.

Up to 20 terms per parameter and 64 distinct filters server-wide; beyond either limit the subscription is rejected with 400. Subscribers using the same filter share a group, so each filter is evaluated once per certificate regardless of how many clients use it, and the already-serialized payload is passed to the group rather than re-rendered per client. A server with no filtered subscribers performs no filter matching.

A filtered subscriber can lose messages in two places: on its own connection, and in the shared dispatcher before matching. Both are reported through the SSE gap event and the lag counters. Dispatcher loss cannot be attributed to a specific filter, so the reported count is raw certificates rather than matches.

Version 2 messages

Off by default. Enable with CERTSTREAM_STREAM_V2_ENABLED=true, then connect to ws://host:8080/v2 or /sse?stream=v2. The v1 streams are the CertStream shape and are unchanged; these fields appear only in v2.

{
  "message_type": "certificate_update",
  "version": 2,
  "data": {
    "source": {
      "name": "Let's Encrypt 'Sycamore2026h2'",
      "url": "https://mon.sycamore.ct.letsencrypt.org/2026h2",
      "log_id": "bP5QGUOoXqkWvFLRM+TcyR7xQRx9JYQg0XOAnhgY6zo=",
      "operator": "lets encrypt",
      "log_type": "static_ct"
    },
    "entry": {
      "index": 1000,
      "type": "x509",
      "observed_at": "2026-09-05T14:42:41.260Z",
      "submitted_at": "2025-09-09T01:01:44.517Z",
      "link": "https://mon.sycamore.ct.letsencrypt.org/2026h2/tile/data/003"
    },
    "verification": {
      "checkpoint_signature": "verified",
      "inclusion": "unverified"
    },
    "cert": { "...": "as in the lite stream" }
  }
}

(source.log_id, entry.index) identifies the record at its source; the certificate's SHA-256 identifies the certificate. One certificate appears in several logs at different indices, so the two are separate keys. source.operator is the operator name as the catalog spells it; the canonical key used by ct_log.operator_rate_limits and the metric labels is that name lowercased with punctuation collapsed.

verification

Records which checks were performed and what they found.

ValueMeaning
verifiedThe check ran and passed
unverifiedThe check did not run, or could not run on this input
failedThe check ran and failed; checkpoint_signature_mode decides whether the entry is still ingested
not_applicableThe check does not apply: an RFC 6962 log serves no checkpoint

Merkle verification

Verify that fetched entries belong to the tree represented by the signed checkpoint.

Off by default, and static-CT only. A valid checkpoint signature authenticates the tree root, but does not by itself prove that recently fetched entries are included in that tree, or that the tree is consistent with one observed earlier. Both properties can be verified from the log's own hash tiles.

ModeWhat it provesCost
offNothing beyond the checkpoint signatureNone
consistencyEach new checkpoint extends the previous one, so the log has not rewritten historyA few hash-tile fetches per checkpoint
fullConsistency, plus that every ingested entry is in the signed treeHash tile fetches for every data tile, from the same per-operator request budget as ingest
CERTSTREAM_STATIC_CT_MERKLE_VERIFICATION=full certstream-server-rust

If a tile cannot be fetched (a 429, a timeout, or a partial tile at the head of the log that has no full tile yet), the entry is treated as unverified rather than invalid: it is marked inclusion: unverified and delivery continues. Only a hash mismatch drops the tile; certstream_static_ct_inclusion_unavailable and certstream_static_ct_inclusion_failed count the two cases.

Verification fetches share the per-operator rate limit with ingest, so ingest gets a smaller share of it. Consistency is anchored on the first checkpoint of each process, so a restart re-anchors.

Names tiles

Some static-CT logs serve an optional extension that publishes the names on the certificates in a data tile as compact JSON lines. Set CERTSTREAM_STATIC_CT_NAMES_TILES=prefer to read those instead of parsing data tiles; logs that do not serve the extension fall back to the normal path automatically.

Names-tile data is not authenticated by the CT tree: the extension cannot be checked for inclusion in a signed tree head. Entries collected this way are published as message_type: "dns_entries_unauthenticated", never as dns_entries. Cross-log dedup is unavailable in this mode, since there is no certificate hash to dedup on.

A names tile carries no certificate, so this mode requires domains_only to be the only enabled stream and cannot be combined with the JetStream output. These combinations are rejected during configuration validation.

NATS JetStream

Durable delivery for consumers that may disconnect.

A WebSocket subscriber misses whatever arrives while it is disconnected. Enable CERTSTREAM_NATS_ENABLED=true and records are also published to JetStream, where a durable consumer resumes from where it stopped.

The saved position follows acknowledgements, not reads, so a restart re-reads anything unacknowledged. Records carry a stable Nats-Msg-Id (<log_id>:<index>), so re-reads are deduplicated by the server. When the stream is full, CERTSTREAM_NATS_ON_FULL chooses block (default, pushes back on ingest) or drop.

Records are published to certstream.<operator>.<log>, so a consumer can subscribe to one operator or one log rather than the whole feed.

Cross-log dedup does not apply to the durable path: a certificate present in three logs produces three records.

Delivery is at-least-once for the entries this server reads, not exactly-once end to end. Records discarded in drop mode are not retried, and entries that fail to parse produce no record.

Historical range export

certstream-server-rust --backfill --log "Sycamore2026h2" --start 1000 --end 2000 --out range.jsonl
FlagMeaning
--logCT log URL, log ID, or a substring of its name. An ambiguous substring is an error, not a guess.
--start, --endEntry index range, both inclusive
--outOutput file; omit it or pass - for stdout

Writes one v2 record per line. The range is fixed up front and the live state file is never written, so a backfill cannot move a running server's position. Exits non-zero if any fetch failed.

Client examples

Python certstream
# pip install certstream
import certstream

def callback(message, context):
    if message['message_type'] == 'certificate_update':
        domains = message['data']['leaf_cert']['all_domains']
        print(domains)

certstream.listen_for_events(callback, url='ws://localhost:8080/')
Go certstream-go
import "github.com/CaliDog/certstream-go"

stream, _ := certstream.CertStreamEventStream(false)
for event := range stream {
    fmt.Println(event.Data.LeafCert.AllDomains)
}
JavaScript Node.js
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8080/');

ws.on('message', (data) => {
    const msg = JSON.parse(data);
    if (msg.message_type === 'certificate_update') {
        console.log(msg.data.leaf_cert.all_domains);
    }
});
Bash with auth
# WebSocket with token
wscat -c ws://localhost:8080/ -H "Authorization: Bearer your-token"

# SSE with token
curl -H "Authorization: Bearer your-token" http://localhost:8080/sse

Run your own instance

Docker recommended
# Quick start
docker run -d -p 8080:8080 \
  ghcr.io/reloading01/certstream-server-rust:latest

# With custom config
docker run -d -p 8080:8080 \
  -v ./config.yaml:/app/config.yaml \
  -v ./state:/app/state \
  ghcr.io/reloading01/certstream-server-rust:latest
Build from source
# Cargo build
cargo build --release
./target/release/certstream-server-rust

# Docker Compose
docker compose up -d
YAML docker-compose.yml
services:
  certstream:
    image: ghcr.io/reloading01/certstream-server-rust:latest
    ports:
      - "8080:8080"
    volumes:
      - ./config.yaml:/app/config.yaml
      - ./state:/app/state
    restart: unless-stopped
Debian / Ubuntu signed apt repository
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://reloading01.github.io/packages/key.gpg \
  | sudo tee /etc/apt/keyrings/certstream.asc > /dev/null
echo "deb [signed-by=/etc/apt/keyrings/certstream.asc] https://reloading01.github.io/packages/apt stable main" \
  | sudo tee /etc/apt/sources.list.d/certstream.list
sudo apt update
sudo apt install certstream-server-rust
sudo systemctl enable --now certstream-server-rust
Fedora / RHEL / openSUSE signed rpm repository
sudo rpm --import https://reloading01.github.io/packages/key.gpg
sudo tee /etc/yum.repos.d/certstream.repo > /dev/null <<'REPO'
[certstream]
name=certstream-server-rust
baseurl=https://reloading01.github.io/packages/rpm
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://reloading01.github.io/packages/key.gpg
REPO
sudo dnf install certstream-server-rust
sudo systemctl enable --now certstream-server-rust

The packaged systemd unit reads settings from /etc/default/certstream-server-rust.

Environment variables

All of the settings below can be set through environment variables.

General

VariableDefaultDescription
CERTSTREAM_HOST0.0.0.0Bind address
CERTSTREAM_PORT8080HTTP / WebSocket port
CERTSTREAM_LOG_LEVELinfodebug, info, warn, error
CERTSTREAM_BUFFER_SIZE1000Broadcast buffer

Protocols

VariableDefaultDescription
CERTSTREAM_WS_ENABLEDtrueEnable WebSocket
CERTSTREAM_SSE_ENABLEDfalseEnable SSE (opt-in)
CERTSTREAM_METRICS_ENABLEDtrueEnable /metrics endpoint
CERTSTREAM_HEALTH_ENABLEDtrueEnable /health endpoint
CERTSTREAM_EXAMPLE_JSON_ENABLEDtrueEnable /example.json endpoint
CERTSTREAM_API_ENABLEDfalseEnable REST API endpoints

Stream types

VariableDefaultDescription
CERTSTREAM_STREAM_FULL_ENABLEDtrueFull stream (DER + chain)
CERTSTREAM_STREAM_LITE_ENABLEDtrueLite stream
CERTSTREAM_STREAM_DOMAINS_ONLY_ENABLEDtrueDomains-only stream

Disabled stream types are not serialized and their routes are not registered.

Connection limiting

VariableDefaultDescription
CERTSTREAM_CONNECTION_LIMIT_ENABLEDfalseEnable connection limits
CERTSTREAM_CONNECTION_LIMIT_MAX_CONNECTIONS10000Max total connections
CERTSTREAM_CONNECTION_LIMIT_PER_IP_LIMIT100Max per IP
CERTSTREAM_TRUSTED_PROXIESnoneComma-separated IP addresses or CIDR ranges of reverse proxies whose X-Forwarded-For and X-Real-IP headers are believed. Without it, every client behind a proxy shares the proxy's address, and the per-IP limits apply to all of them together

Authentication

VariableDefaultDescription
CERTSTREAM_AUTH_ENABLEDfalseEnable token auth
CERTSTREAM_AUTH_TOKENSnoneComma-separated tokens
CERTSTREAM_AUTH_HEADER_NAMEAuthorizationAuth header

Rate limiting

VariableDefaultDescription
CERTSTREAM_RATE_LIMIT_ENABLEDfalseEnable per-IP rate limiting (token bucket + sliding window)

CT log settings

VariableDefaultDescription
CERTSTREAM_CT_LOG_STATE_FILEcertstream_state.jsonState file path
CERTSTREAM_CT_LOG_RETRY_MAX_ATTEMPTS3Max retry attempts
CERTSTREAM_CT_LOG_REQUEST_TIMEOUT_SECS30Request timeout
CERTSTREAM_CT_LOG_BATCH_SIZE1024Entries requested per get-entries call (servers clamp to their own max)
CERTSTREAM_CT_LOG_FETCH_CONCURRENCY4Concurrent range/tile fetches per watcher during catch-up (1-16)
CERTSTREAM_CT_LOG_PARTIAL_TILE_WAIT_SECS60Seconds a caught-up static-CT watcher waits for the newest partial tile to fill before fetching it. A full tile is fetched once and cached by the operator, each width of a partial tile is a new uncached request. The last 0 to 255 entries arrive up to this long later; 0 fetches partial tiles at once (maximum 600)
CERTSTREAM_CT_LOG_MAX_CATCHUP_LAG_ENTRIES0A watcher more than this many entries behind its log skips ahead to near the head at its next head refresh, logs the number of entries skipped and counts them in certstream_ct_log_skipped_entries_total. 0 reads every entry however long it takes; the minimum is 10000
CERTSTREAM_CT_LOG_MAX_CATCHUP_LAG_SECS0The same bound as the age of the newest entry the watcher has read, capped by how long the watcher has been behind its log's head, so a log that publishes its head late is not skipped for that. 0 never skips; the minimum is 300. A log that was offline for longer than this skips the gap on restart
CERTSTREAM_USER_AGENTcertstream-server-rust/{VERSION} (+https://github.com/reloading01/certstream-server-rust)User-Agent for all outbound HTTP. The default carries the project URL, which is what Geomys asks for: it applies a global limit of 75 requests per second to clients that give no email address or URL, and none to those that do. Set your own contact if you want the operator to reach you rather than the project. GoDaddy limits by the product and version at the start of the string, and the default is shared by every install, so its logs are often refused with a 429: a name of your own, such as yourname-monitor/1.0 (you@example.com), avoids it. A blank value falls back to the default.
CERTSTREAM_CT_LOG_FORCE_HTTP1_OPERATORSnoneComma-separated operators whose watchers fetch over HTTP/1.1 instead of HTTP/2
CERTSTREAM_CT_LOG_OPERATOR_RATE_LIMITScloudflare=125Comma-separated operator=milliseconds floors on the request interval, for example trustasia=10,digicert=40. Replaces ct_log.operator_rate_limits; an entry without a positive number is ignored
CERTSTREAM_CT_LOG_EXCLUDED_OPERATORSnoneComma-separated operators whose catalog logs are not monitored, for example geomys,godaddy. Excluded logs are not contacted at all; logs in static_logs are unaffected
CERTSTREAM_CT_LOG_EXCLUDED_LOGSnoneComma-separated monitoring URLs of single catalog logs that are not monitored, as /api/logs shows them. An entry that matches no log is reported in the startup log
CERTSTREAM_STATIC_CT_CHECKPOINT_SIGNATUREwarnCheckpoint signature policy: warn or enforce
CERTSTREAM_DEDUP_CAPACITY200000Cross-log dedup capacity
CERTSTREAM_DEDUP_TTL_SECS900Dedup window (seconds)

CERTSTREAM_CT_LOG_FORCE_HTTP1_OPERATORS changes how many TCP connections concurrent fetches use. On its own it does not imply higher throughput.

Verification and collection

VariableDefaultDescription
CERTSTREAM_CT_LOG_STATE_RECOVERYfreshUnreadable state file: fresh restarts from the log head, fail refuses to start
CERTSTREAM_CT_LOG_REFRESH_INTERVAL_SECS3600Log-list refresh interval while running; 0 disables it
CERTSTREAM_CT_LOG_REMOVED_POLICYstopWhat a refresh does with a delisted log: stop or keep
CERTSTREAM_STATIC_CT_MERKLE_VERIFICATIONoffoff, consistency, or full
CERTSTREAM_STATIC_CT_NAMES_TILESoffoff or prefer
CERTSTREAM_STREAM_V2_ENABLEDfalseServe the version 2 output

NATS JetStream

VariableDefaultDescription
CERTSTREAM_NATS_ENABLEDfalsePublish records to JetStream
CERTSTREAM_NATS_URLnats://127.0.0.1:4222Broker URL
CERTSTREAM_NATS_STREAMCERTSTREAMStream name
CERTSTREAM_NATS_SUBJECT_PREFIXcertstreamSubjects are <prefix>.<operator>.<log>
CERTSTREAM_NATS_MAX_BYTES8589934592Stream size cap (8 GiB)
CERTSTREAM_NATS_ON_FULLblockblock holds the position back, drop gives up on the record

Hot reload

VariableDefaultDescription
CERTSTREAM_HOT_RELOAD_ENABLEDfalseEnable hot reload
CERTSTREAM_HOT_RELOAD_WATCH_PATHnoneConfiguration file to watch

YAML config file

YAML config.yaml
host: "0.0.0.0"
port: 8080
log_level: "info"
buffer_size: 1000

protocols:
  websocket: true
  sse: false
  metrics: true
  health: true
  example_json: true
  api: false

streams:
  full: true
  lite: true
  domains_only: true

ct_log:
  state_file: "/data/state.json"
  batch_size: 1024
  fetch_concurrency: 4
  poll_interval_ms: 500
  retry_max_attempts: 3
  request_timeout_secs: 30
  user_agent: "certstream-server-rust (security@example.com)"

dedup:
  capacity: 200000
  ttl_secs: 900

connection_limit:
  enabled: true
  max_connections: 10000
  per_ip_limit: 100

auth:
  enabled: false
  tokens:
    - "secret-token-1"
  header_name: "Authorization"

rate_limit:
  enabled: false
  max_tokens: 100
  refill_rate: 10

hot_reload:
  enabled: true

# Static CT logs (static-ct-api protocol).
# Use the monitoring prefix (mon.*) for read access.
# Trusted logs are also auto-discovered from the Google + Apple log lists.
static_logs:
  - name: "Let's Encrypt 'Willow' 2026h1"
    url: "https://mon.willow.ct.letsencrypt.org/2026h1/"
  - name: "Let's Encrypt 'Sycamore' 2026h1"
    url: "https://mon.sycamore.ct.letsencrypt.org/2026h1/"

Config search order: CERTSTREAM_CONFIG env var → ./config.yaml → ./config.yml → /etc/certstream/config.yaml

Logs without a checkpoint

Some RFC 6962 logs serve static-CT tiles but no /checkpoint. Set tree_size_source: get_sth on a static_logs entry to take the tree size from get-sth and read entries from the tiles. The server cannot verify such a log itself.

Forcing HTTP/1.1

Some operators limit per TCP connection, and HTTP/2 shares one connection across watchers. Operators listed under ct_log.force_http1_operators get their own HTTP/1.1 connections. The default is [digicert, microsec]: DigiCert throttles per connection, and Microsec resets HTTP/2 streams under concurrent tile fetches. Set it to [] to use HTTP/2 everywhere. The request rate does not change.

Per-operator pacing

Each operator gets one request budget shared by all of its logs: ct_log.default_operator_rate_limit_ms (25 ms) unless ct_log.operator_rate_limits names it. The default map holds Cloudflare to 125 ms, because Cloudflare blocks an IP for a minute after 100 requests in 10 seconds. Setting the map replaces the default, so keep cloudflare: 125 when adding an operator; {} removes it.

Performance

v1.6.0 measured over a 3-hour run with the default configuration, all four stream formats enabled, and two WebSocket subscribers (one filtered).

335msg/s
Delivered throughput, 5.4 GB out
4.6%
CPU, mean of one core
168MB
RSS, mean (196 MB peak)
0
Messages missed by subscribers

Throughput does not say how current the stream is. Watch certstream_ct_log_lag_entries and certstream_ct_log_ingest_delay_seconds per log; sustained growth is the signal. Results vary by host and workload.

Prometheus metrics

Core

MetricTypeDescription
certstream_messages_sentcounterTotal certificate messages broadcast
certstream_parse_failurescounterFailed certificate parses (RFC 6962)
certstream_ct_logs_countgaugeRFC 6962 CT logs monitored
certstream_worker_panicscounterWorker panics. The watcher is restarted 5 seconds later

Static CT

MetricTypeDescription
certstream_static_ct_logs_countgaugeStatic CT logs monitored
certstream_static_ct_tiles_fetchedcounterTiles fetched from static CT logs
certstream_static_ct_entries_parsedcounterEntries parsed from tiles
certstream_static_ct_checkpoint_errorscounterCheckpoint fetch / parse failures
certstream_static_ct_checkpoint_sig_verifiedcounterCheckpoint signatures verified
certstream_static_ct_checkpoint_sig_failedcounterCheckpoint signatures that failed verification
certstream_static_ct_checkpoint_sig_unverifiablecounterCheckpoints with no usable verification key
certstream_issuer_cache_sizegaugeCached issuer certificates
certstream_duplicates_filteredcounterDuplicate certificates filtered by dedup
certstream_dedup_cache_sizegaugeCurrent dedup cache entries
certstream_dedup_effective_ttl_secondsgaugeDeduplication window in force, which falls below dedup.ttl_secs when the cache is over capacity

Log lag

A healthy log is not necessarily a log the watcher is keeping up with. Health status tells you whether requests are succeeding. Lag tells you whether the local position is falling behind the log head. For alerting, sustained growth in lag is more useful than an absolute value, since a watcher legitimately has lag while catching up after a restart.

MetricTypeDescription
certstream_ct_log_lag_entriesgaugeEntries between the watcher's position and the log's head, per log
certstream_ct_log_rate_limited_totalcounterRequests answered with 429, per log
certstream_ct_log_empty_responses_totalcounterget-entries responses containing no entries
certstream_issuer_cache_hits / _missescounterIssuer cache lookups
certstream_issuer_fetch_attemptscounterIssuers retrieved over the network, which a cache miss does not always cause

Bandwidth

MetricTypeDescription
certstream_bytes_sent_totalcounterBytes written to subscribers, labelled by protocol
certstream_bytes_serialized_totalcounterBytes produced during serialization

Serialization is counted once per certificate for each enabled stream format. If serialized bytes stay high for a stream nobody consumes, disable that stream type under streams so it is not serialized.

Memory

RSS alone does not show how much memory the application currently has allocated. The jemalloc counters below separate the two: a gap between allocated and resident is allocator behaviour, while growth in allocated is the application holding data.

MetricTypeDescription
certstream_jemalloc_allocated_bytesgaugeBytes currently allocated by the application
certstream_jemalloc_resident_bytesgaugeResident pages accounted for by jemalloc
certstream_jemalloc_active_bytesgaugeActive pages. The excess over allocated is size-class rounding
certstream_jemalloc_mapped_bytesgaugeAddress space mapped by jemalloc
certstream_jemalloc_retained_bytesgaugeVirtual address space retained by jemalloc after the pages were returned to the OS
certstream_jemalloc_metadata_bytesgaugejemalloc metadata, which scales with the arena count

The current workload settles at roughly 80 MB RSS while processing about 420 certificates per second. If process RSS is much higher than jemalloc's allocated or resident counters, check the host's Transparent Huge Pages setting with cat /sys/kernel/mm/transparent_hugepage/enabled.

jemalloc settings can be overridden without rebuilding through _RJEM_MALLOC_CONF.

CT logs monitored

The server monitors Chrome- and Apple-trusted Certificate Transparency logs and deduplicates certificates that appear in more than one log. The list below is the set at the time of writing; the catalogs change and the server follows them hourly.

Google

Argon, Xenon, ParcelYard (static CT), PlumbersArms (static CT)

Cloudflare

Nimbus

DigiCert

Wyvern, Sphinx

Sectigo

Elephant, Tiger, Mammoth, Sabre

Let's Encrypt

Willow, Sycamore (static CT)

TrustAsia

log2026a and b, HETU, Luoshu (static CT)

Geomys

Tuscolo, Trastevere (static CT)

IPng Networks

Halloumi, Gouda (static CT)

Microsec

Eszigno (static CT)

GoDaddy

Aquamarine (static CT)