Certstream API
WebSocket and SSE endpoints for consuming the certificate stream, plus configuration for self-hosted instances.
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-jsonis 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.
{
"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.
{
"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
| Endpoint | Description |
|---|---|
ws://host:8080/ | Lite stream (no DER / chain) |
ws://host:8080/full-stream | Full data with DER and chain |
ws://host:8080/domains-only | Domain names only (message_type: "dns_entries") |
ws://host:8080/v2 | Version 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
| Endpoint | Description |
|---|---|
http://host:8080/sse | Lite (default) |
http://host:8080/sse?stream=full | Full |
http://host:8080/sse?stream=domains | Domains only |
http://host:8080/sse?stream=v2 | Version 2 output (off by default) |
HTTP
| Endpoint | Description |
|---|---|
/health | Basic health check (returns "OK") |
/health/deep | Detailed health: log status, connections, uptime (JSON) |
/metrics | Prometheus metrics |
/example.json | Example message |
REST API
Disabled by default. Enable with CERTSTREAM_API_ENABLED=true.
| Endpoint | Description |
|---|---|
GET /api/stats | Server statistics (uptime, connections, throughput, cache) |
GET /api/logs | CT 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'
| Parameter | Matches |
|---|---|
domain | The 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. |
issuer | Case-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.
| Value | Meaning |
|---|---|
verified | The check ran and passed |
unverified | The check did not run, or could not run on this input |
failed | The check ran and failed; checkpoint_signature_mode decides whether the entry is still ingested |
not_applicable | The 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.
| Mode | What it proves | Cost |
|---|---|---|
off | Nothing beyond the checkpoint signature | None |
consistency | Each new checkpoint extends the previous one, so the log has not rewritten history | A few hash-tile fetches per checkpoint |
full | Consistency, plus that every ingested entry is in the signed tree | Hash 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
| Flag | Meaning |
|---|---|
--log | CT log URL, log ID, or a substring of its name. An ambiguous substring is an error, not a guess. |
--start, --end | Entry index range, both inclusive |
--out | Output 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
# 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/')
import "github.com/CaliDog/certstream-go" stream, _ := certstream.CertStreamEventStream(false) for event := range stream { fmt.Println(event.Data.LeafCert.AllDomains) }
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); } });
# 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
# 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
# Cargo build cargo build --release ./target/release/certstream-server-rust # Docker Compose docker compose up -d
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
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
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
| Variable | Default | Description |
|---|---|---|
CERTSTREAM_HOST | 0.0.0.0 | Bind address |
CERTSTREAM_PORT | 8080 | HTTP / WebSocket port |
CERTSTREAM_LOG_LEVEL | info | debug, info, warn, error |
CERTSTREAM_BUFFER_SIZE | 1000 | Broadcast buffer |
Protocols
| Variable | Default | Description |
|---|---|---|
CERTSTREAM_WS_ENABLED | true | Enable WebSocket |
CERTSTREAM_SSE_ENABLED | false | Enable SSE (opt-in) |
CERTSTREAM_METRICS_ENABLED | true | Enable /metrics endpoint |
CERTSTREAM_HEALTH_ENABLED | true | Enable /health endpoint |
CERTSTREAM_EXAMPLE_JSON_ENABLED | true | Enable /example.json endpoint |
CERTSTREAM_API_ENABLED | false | Enable REST API endpoints |
Stream types
| Variable | Default | Description |
|---|---|---|
CERTSTREAM_STREAM_FULL_ENABLED | true | Full stream (DER + chain) |
CERTSTREAM_STREAM_LITE_ENABLED | true | Lite stream |
CERTSTREAM_STREAM_DOMAINS_ONLY_ENABLED | true | Domains-only stream |
Disabled stream types are not serialized and their routes are not registered.
Connection limiting
| Variable | Default | Description |
|---|---|---|
CERTSTREAM_CONNECTION_LIMIT_ENABLED | false | Enable connection limits |
CERTSTREAM_CONNECTION_LIMIT_MAX_CONNECTIONS | 10000 | Max total connections |
CERTSTREAM_CONNECTION_LIMIT_PER_IP_LIMIT | 100 | Max per IP |
CERTSTREAM_TRUSTED_PROXIES | none | Comma-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
| Variable | Default | Description |
|---|---|---|
CERTSTREAM_AUTH_ENABLED | false | Enable token auth |
CERTSTREAM_AUTH_TOKENS | none | Comma-separated tokens |
CERTSTREAM_AUTH_HEADER_NAME | Authorization | Auth header |
Rate limiting
| Variable | Default | Description |
|---|---|---|
CERTSTREAM_RATE_LIMIT_ENABLED | false | Enable per-IP rate limiting (token bucket + sliding window) |
CT log settings
| Variable | Default | Description |
|---|---|---|
CERTSTREAM_CT_LOG_STATE_FILE | certstream_state.json | State file path |
CERTSTREAM_CT_LOG_RETRY_MAX_ATTEMPTS | 3 | Max retry attempts |
CERTSTREAM_CT_LOG_REQUEST_TIMEOUT_SECS | 30 | Request timeout |
CERTSTREAM_CT_LOG_BATCH_SIZE | 1024 | Entries requested per get-entries call (servers clamp to their own max) |
CERTSTREAM_CT_LOG_FETCH_CONCURRENCY | 4 | Concurrent range/tile fetches per watcher during catch-up (1-16) |
CERTSTREAM_CT_LOG_PARTIAL_TILE_WAIT_SECS | 60 | Seconds 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_ENTRIES | 0 | A 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_SECS | 0 | The 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_AGENT | certstream-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_OPERATORS | none | Comma-separated operators whose watchers fetch over HTTP/1.1 instead of HTTP/2 |
CERTSTREAM_CT_LOG_OPERATOR_RATE_LIMITS | cloudflare=125 | Comma-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_OPERATORS | none | Comma-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_LOGS | none | Comma-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_SIGNATURE | warn | Checkpoint signature policy: warn or enforce |
CERTSTREAM_DEDUP_CAPACITY | 200000 | Cross-log dedup capacity |
CERTSTREAM_DEDUP_TTL_SECS | 900 | Dedup 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
| Variable | Default | Description |
|---|---|---|
CERTSTREAM_CT_LOG_STATE_RECOVERY | fresh | Unreadable state file: fresh restarts from the log head, fail refuses to start |
CERTSTREAM_CT_LOG_REFRESH_INTERVAL_SECS | 3600 | Log-list refresh interval while running; 0 disables it |
CERTSTREAM_CT_LOG_REMOVED_POLICY | stop | What a refresh does with a delisted log: stop or keep |
CERTSTREAM_STATIC_CT_MERKLE_VERIFICATION | off | off, consistency, or full |
CERTSTREAM_STATIC_CT_NAMES_TILES | off | off or prefer |
CERTSTREAM_STREAM_V2_ENABLED | false | Serve the version 2 output |
NATS JetStream
| Variable | Default | Description |
|---|---|---|
CERTSTREAM_NATS_ENABLED | false | Publish records to JetStream |
CERTSTREAM_NATS_URL | nats://127.0.0.1:4222 | Broker URL |
CERTSTREAM_NATS_STREAM | CERTSTREAM | Stream name |
CERTSTREAM_NATS_SUBJECT_PREFIX | certstream | Subjects are <prefix>.<operator>.<log> |
CERTSTREAM_NATS_MAX_BYTES | 8589934592 | Stream size cap (8 GiB) |
CERTSTREAM_NATS_ON_FULL | block | block holds the position back, drop gives up on the record |
Hot reload
| Variable | Default | Description |
|---|---|---|
CERTSTREAM_HOT_RELOAD_ENABLED | false | Enable hot reload |
CERTSTREAM_HOT_RELOAD_WATCH_PATH | none | Configuration file to watch |
YAML config file
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).
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
| Metric | Type | Description |
|---|---|---|
certstream_messages_sent | counter | Total certificate messages broadcast |
certstream_parse_failures | counter | Failed certificate parses (RFC 6962) |
certstream_ct_logs_count | gauge | RFC 6962 CT logs monitored |
certstream_worker_panics | counter | Worker panics. The watcher is restarted 5 seconds later |
Static CT
| Metric | Type | Description |
|---|---|---|
certstream_static_ct_logs_count | gauge | Static CT logs monitored |
certstream_static_ct_tiles_fetched | counter | Tiles fetched from static CT logs |
certstream_static_ct_entries_parsed | counter | Entries parsed from tiles |
certstream_static_ct_checkpoint_errors | counter | Checkpoint fetch / parse failures |
certstream_static_ct_checkpoint_sig_verified | counter | Checkpoint signatures verified |
certstream_static_ct_checkpoint_sig_failed | counter | Checkpoint signatures that failed verification |
certstream_static_ct_checkpoint_sig_unverifiable | counter | Checkpoints with no usable verification key |
certstream_issuer_cache_size | gauge | Cached issuer certificates |
certstream_duplicates_filtered | counter | Duplicate certificates filtered by dedup |
certstream_dedup_cache_size | gauge | Current dedup cache entries |
certstream_dedup_effective_ttl_seconds | gauge | Deduplication 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.
| Metric | Type | Description |
|---|---|---|
certstream_ct_log_lag_entries | gauge | Entries between the watcher's position and the log's head, per log |
certstream_ct_log_rate_limited_total | counter | Requests answered with 429, per log |
certstream_ct_log_empty_responses_total | counter | get-entries responses containing no entries |
certstream_issuer_cache_hits / _misses | counter | Issuer cache lookups |
certstream_issuer_fetch_attempts | counter | Issuers retrieved over the network, which a cache miss does not always cause |
Bandwidth
| Metric | Type | Description |
|---|---|---|
certstream_bytes_sent_total | counter | Bytes written to subscribers, labelled by protocol |
certstream_bytes_serialized_total | counter | Bytes 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.
| Metric | Type | Description |
|---|---|---|
certstream_jemalloc_allocated_bytes | gauge | Bytes currently allocated by the application |
certstream_jemalloc_resident_bytes | gauge | Resident pages accounted for by jemalloc |
certstream_jemalloc_active_bytes | gauge | Active pages. The excess over allocated is size-class rounding |
certstream_jemalloc_mapped_bytes | gauge | Address space mapped by jemalloc |
certstream_jemalloc_retained_bytes | gauge | Virtual address space retained by jemalloc after the pages were returned to the OS |
certstream_jemalloc_metadata_bytes | gauge | jemalloc 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.
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)

