Certstream API Documentation
Connect to real-time certificate streams via WebSocket or SSE
What is Certstream?
Real-time certificate transparency log aggregation
Overview
Certstream aggregates certificates from Certificate Transparency logs and streams them in real-time. This Rust implementation is a drop-in replacement for certstream-server (Elixir) and certstream-server-go with better performance.
Why Rust?
- ~150MB stable RSS (flat footprint under sustained load — no growth over time)
- Zero-copy broadcast via
Arc<PreSerializedMessage>— serialize once, deliver to all - Handles 50,000+ concurrent connections
- SIMD-accelerated JSON serialization via
simd-json(enabled by default) - Single binary, no dependencies
Message Format
JSON structure for certificate update messages
{
"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,
"source": {
"url": "ct.googleapis.com/logs/argon2025",
"name": "Google Argon 2025"
}
}
}
Domains-Only Message Format
The /domains-only WebSocket and /sse?stream=domains endpoints use a distinct schema
{
"message_type": "dns_entries",
"data": [
"example.com",
"www.example.com",
"*.example.com"
]
}
Unlike the full/lite streams, message_type is "dns_entries" (not "certificate_update"),
and data is a bare JSON array of strings — not an object with nested fields.
Use this stream when you only need domain names and want minimal bandwidth.
Stream Types
Choose the right stream for your use case
Lite Stream (default)
Contains certificate metadata, domains, issuer info, and timestamps. No DER-encoded certificate data or chain. Best for most monitoring use cases.
Full Stream
Includes everything in lite stream plus base64-encoded DER certificate and full chain. Use when you need to verify or store complete certificates.
Domains Only
Minimal payload with just domain names. Lowest bandwidth option for domain-focused monitoring like phishing detection. Note: the schema differs from the other streams — message_type is "dns_entries" and data is a bare array of strings, not an object.
Quick Start
Connect and start receiving certificates in seconds
const ws = new WebSocket('ws://localhost:8080/'); ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.message_type === 'certificate_update') { console.log(data.data.leaf_cert.all_domains); } };
# Lite stream curl -N http://localhost:8080/sse # Full stream curl -N "http://localhost:8080/sse?stream=full" # Domains only curl -N "http://localhost:8080/sse?stream=domains"
Client Examples
Connect using your preferred language
# 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
Self-Hosting
Run your own instance with Docker or from source
# Quick start docker run -d -p 8080:8080 -p 8081:8081 ghcr.io/reloading01/certstream-server-rust:latest # With custom config docker run -d \ -p 8080:8080 \ -p 8081:8081 \ -v ./config.yaml:/app/config.yaml \ -v ./state:/app/state \ ghcr.io/reloading01/certstream-server-rust:latest # Docker Compose version: '3.8' services: certstream: image: ghcr.io/reloading01/certstream-server-rust:latest ports: - "8080:8080" - "8081:8081" volumes: - ./config.yaml:/app/config.yaml - ./state:/app/state restart: unless-stopped
# Docker build docker build -t certstream-server-rust . docker run -d -p 8080:8080 certstream-server-rust # Cargo build cargo build --release ./target/release/certstream-server-rust # Docker Compose docker compose up -d
Environment Variables
Configure the server using environment variables
General Settings
| 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 |
true | Enable SSE |
CERTSTREAM_METRICS_ENABLED |
true | Enable /metrics endpoint |
CERTSTREAM_HEALTH_ENABLED |
true | Enable /health endpoint |
CERTSTREAM_EXAMPLE_JSON_ENABLED |
true | Enable /example.json endpoint |
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 |
Authentication
| Variable | Default | Description |
|---|---|---|
CERTSTREAM_AUTH_ENABLED |
false | Enable token auth |
CERTSTREAM_AUTH_TOKENS |
- | Comma-separated tokens |
CERTSTREAM_AUTH_HEADER_NAME |
Authorization | Auth header |
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 |
256 | Entries per batch |
Hot Reload
| Variable | Default | Description |
|---|---|---|
CERTSTREAM_HOT_RELOAD_ENABLED |
false | Enable hot reload |
CERTSTREAM_HOT_RELOAD_WATCH_PATH |
- | Config file to watch |
Configuration File
Full YAML configuration example
host: "0.0.0.0" port: 8080 log_level: "info" buffer_size: 1000 ct_logs_url: "https://www.gstatic.com/ct/log_list/v3/log_list.json" protocols: websocket: true sse: true metrics: true health: true example_json: true ct_log: state_file: "/data/state.json" batch_size: 256 poll_interval_ms: 500 retry_max_attempts: 3 request_timeout_secs: 30 connection_limit: enabled: true max_connections: 10000 per_ip_limit: 100 auth: enabled: false tokens: - "secret-token-1" - "secret-token-2" header_name: "Authorization" hot_reload: enabled: true custom_logs: - name: "My Custom CT Log" url: "https://ct.example.com/log" # Static CT logs (Sunlight / static-ct-api protocol) # Use monitoring prefix (mon.*) for read access static_logs: - name: "Let's Encrypt 'Willow' 2025h2d" url: "https://mon.willow.ct.letsencrypt.org/2025h2d/" - name: "Let's Encrypt 'Willow' 2026h1" url: "https://mon.willow.ct.letsencrypt.org/2026h1/" - name: "Let's Encrypt 'Sycamore' 2025h2d" url: "https://mon.sycamore.ct.letsencrypt.org/2025h2d/" - 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
Performance
Load tested with 500 concurrent WebSocket clients
| Metric | Rust | Go |
|---|---|---|
| Memory (idle) | 27 MB | ~49 MB |
| Memory (under load, stable) | ~150 MB | ~309 MB |
| CPU (avg under load) | 23% | 72% |
| Latency (avg) | 8.4ms | 9.2ms |
| P99 Latency | 172ms | 187ms |
| Broadcast throughput | 48.6K/s | 27K/s |
Result (v1.3.0): ~2x less memory under load (flat RSS, no growth over time), 1.8x higher broadcast throughput, zero-copy pipeline via Arc<PreSerializedMessage>.
Prometheus Metrics
Key metrics available at the /metrics endpoint
Core Metrics
| 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 | Number of RFC 6962 CT logs monitored |
certstream_worker_panics |
counter | Worker panic count (auto-recovered) |
Static CT Metrics (v1.3.0)
| Metric | Type | Description |
|---|---|---|
certstream_static_ct_logs_count |
gauge | Number of static CT logs monitored |
certstream_static_ct_tiles_fetched |
counter | Total tiles fetched from static CT logs |
certstream_static_ct_entries_parsed |
counter | Entries successfully parsed from tiles |
certstream_static_ct_checkpoint_errors |
counter | Checkpoint fetch/parse failures |
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 |
CT Logs Monitored
49+ RFC 6962 logs + static CT logs with cross-log deduplication
Argon, Xenon, Solera, Submariner
Cloudflare
Nimbus
DigiCert
Wyvern, Sphinx
Sectigo
Elephant, Tiger, Dodo
Let's Encrypt (Static CT)
Willow 2025h2/2026h1, Sycamore 2025h2/2026h1
Others
TrustAsia, Nordu, and more
certstream-server-rust