Certificate Transparency: Fixing the Trust Model’s Operational Bottleneck

BLOG-2633 Feature Image

Any public certification authority (CA) can issue a certificate for any website on the Internet. That is the design of the WebPKI, but it is also its vulnerability: a browser trusts certificates from any CA in its root store, meaning a single compromised or dishonest CA can impersonate any site. Certificate Transparency (CT) was created to close this gap by making every issued certificate publicly visible and auditable.

CT logs are append-only lists of certificates. They issue Signed Certificate Timestamps (SCTs) as a promise to incorporate a submitted certificate within a set grace period, known as the Maximum Merge Delay (MMD). The system relies on several distinct actors: CAs that submit certificates, browser clients that enforce CT policies, log operators that maintain the logs, and third-party monitors that continuously verify log consistency and check for mis-issuance.

Why the Current CT Log Design Strains Operators

Running a CT log is operationally demanding. Chrome, Safari, and Firefox all impose similar requirements on logs they trust, centered on integrity and availability. Integrity demands are unforgiving: a log must present a single consistent history to all clients for its entire lifetime, and any SCT it issues must eventually result in the certificate being incorporated. A single bit-flip from hardware failure can disqualify a log. Software updates carry similar risk, since a bug causing a correctness violation cannot simply be rolled back. One of the biggest risks is a log that fails to commit SCT-issued certificates to durable storage before losing them.

Availability requirements add further pressure. Chrome’s policy mandates at least 99% uptime over any 90-day rolling period for each API endpoint, and all SCT-issued entries must be merged within the MMD (24 hours in Chrome’s policy). The dynamic nature of the current CT read API makes this hard. The get-entries endpoint permits requests for arbitrary ranges, and get-proof-by-hash asks the server to construct inclusion proofs on demand. Serving such requests requires databases of 5–10 TB and an infrastructure capable of handling tens of millions of requests per day, driving up cost and bandwidth consumption substantially.

The write side strains operators just as much. MMD violations are common: if the rate of incoming submissions exceeds the merge rate, backlogs grow. Operators typically respond by rate-limiting or disabling write endpoints, but that risks violating uptime requirements. Cloudflare’s own Nimbus logs suffered a prolonged outage in November 2023 after a total power loss in the hosting datacenter. The operational bar is so high that only a handful of organizations — Cloudflare among them — operate publicly trusted CT logs, making the ecosystem fragile and vulnerable to the loss of even one or two participants.

A tiled, static approach to CT logs

The static CT API—first implemented in Let’s Encrypt’s Sunlight, announced in May 2024—reorganizes Certificate Transparency data into static, cacheable tiles. The design, influenced by the Go checksum database, lets log operators serve read traffic from S3-compatible object storage and CDN infrastructure rather than dedicated API servers. This cuts operational costs, improves availability, and reduces the surface for integrity violations.

The API is divided into two halves. The monitoring APIs handle reads and are designed for CT monitors as primary clients. The submission APIs cover adding new certificates and remain compatible with RFC 6962, so TLS clients and CAs can submit without modification.

Efficiency from tiling and deduplication

Log data is organized as a series of tiles that clients can fetch selectively to retrieve entries or reconstruct proofs. Operators no longer need to run always-on API servers for reads—only static storage and a CDN cache in front of it.

The format also removes redundant storage of issuer certificates. Since the number of publicly-trusted issuers is small (in the low thousands), each log entry stores only an issuer hash rather than the full intermediate and root certificates. Clients retrieve the actual certificates from a separate endpoint using that hash.

Sequencing before signing

One significant behavioral change: the static CT specification requires logs to batch and sequence submissions before returning an SCT. The response includes a mandatory SCT extension carrying the entry’s index in the log. SCT issuance is delayed by seconds, but this eliminates the Merge Delay that has been a persistent operational burden for CT log operators.

Having the index embedded in the SCT also simplifies auditing. In the RFC 6962 model, checking a certificate’s presence without knowing its index requires a server-side hash-to-index lookup via the get-proof-by-hash endpoint. With the static API, clients holding an SCT can go directly to the tile covering that index—and can apply privacy-preserving auditing techniques when doing so.

Adoption has been rapid. Besides Sunlight and Cloudflare’s new Azul log, two independent implementations exist: Itko and Trillian Tessera. Monitors including crt.sh, certspotter, Censys, and Cloudflare’s own Merkle Town support the format, and Chrome began accepting static CT API logs into its CT log program on April 1, 2025.

Squeezing a CT log onto Workers

Cloudflare’s prototype static CT logs, Cloudflare Research 2025h1a and Cloudflare Research 2025h2a (covering certificates expiring in the first and second half of 2025, respectively), are built on an open-source implementation called Azul. The project’s goal was to run CT logs on Cloudflare’s own edge infrastructure, using only features and limits available to any customer of the Developer Platform. The implementation is written in Rust, targeting the Workers bindings, and ports several C2SP specification helpers from Go to reusable Rust crates.

Performance targets were set against existing production logs: Nimbus2025 handles roughly 33 million read requests per day (~380/s) and about 6 million write requests per day (~70/s).

From a single Go process to distributed components

Azul draws heavily on Sunlight, a Go-based CT log server. A Sunlight deployment is a single process backed by three storage tiers:

  • A strongly consistent “lock backend” storing the latest checkpoint per log, with trivial data volume.
  • A per-log object storage bucket for tiles, checkpoints, and issuer certificates, needing strong consistency and multi-terabyte capacity.
  • A per-log best-effort deduplication cache, holding tens to hundreds of gigabytes, used to avoid re-sequencing previously submitted certificates.

The application logic itself splits into a frontend HTTP server—validating submissions, checking the dedupe cache, and adding entries to a pending pool—and a sequencer that periodically (default every 1s) flushes the pool to new tiles, persists the checkpoint, and notifies waiting requests.

BLOG-2633 Image 1

Mapping components to Workers primitives

The static CT monitoring APIs serve immutable, cacheable assets, making Cloudflare R2 the obvious choice—it provides global consistency, large capacity, configurable caching and compression, and unlimited reads.

BLOG-2633 Image 2

The submission APIs are the harder problem. A frontend Worker runs near the client, handling request validation, dedupe checks, and handoff for sequencing. The actual sequencing—stateful and tightly coordinated—runs in a Durable Object (DO) per log. DO storage doubles as the lock backend for the latest checkpoint, and an alarm triggers sequencing every second. Location hints place the DO near clients, mirroring Google’s Argon/Xenon approach.

The deduplication cache didn’t map cleanly to any single Cloudflare datastore. Sunlight uses a local SQLite database tightly coupled with sequencing; at current submission rates that cache can exceed 50 GB over six months. DO storage and single-database D1 can’t hold that volume, and remote reads and writes inside the sequencing loop were too slow. Azul splits the problem: a fixed-size in-memory cache handles deduplication for recently seen entries (minutes), while Workers KV—eventually consistent, without storage limits—holds the long-term cache.

Porting the Go logic to Rust with this architecture produced a functional log, but numbers exposed the weaknesses. Sequencing topped out at 20–30 entries per second, below the 70/s of existing logs; running more logs for parallel throughput would burden TLS clients and monitors with extra per-log state. Worse, the alarm-driven sequencer often slipped by multiple seconds, so the log couldn’t produce tree heads at steady intervals.

Adding a batching layer

The core problem: one single-threaded DO was juggling connection handling, sequencing, and many async storage writes (10+ object writes plus a KV update per entry). At 100 requests per second, that’s over 100 concurrent tasks fighting for one thread.

BLOG-2633 Image 3

Azul’s fix inserts a layer of “Batcher” DOs between the frontend Worker and the Sequencer. The Worker picks a Batcher via consistent hashing on the cache key. Batchers buffer submissions, coalesce them into batch requests to the Sequencer, write the dedupe cache updates, and route responses back to the waiting Workers. This shrinks the synchronous critical section to what the Sequencer alone must do, letting DOs scale horizontally where workloads are independent.

The result: the submission APIs now handle upwards of 500 requests per second while holding a steady sequencing tempo, keeping per-request latency typically between 1 and 2 seconds.

Rust on Workers: practical notes

The workers-rs bindings track the JavaScript APIs but lag in feature parity. Developers will find documented features missing or only partially implemented in some cases. There are also surprises in the other direction: tokio::sync::watch channels work seamlessly, despite a warning suggesting they shouldn’t. Documentation on debugging and profiling Rust Workers was unclear—preserving debug symbols, for instance, requires care—but the tooling does work. These gaps are expected as the platform evolves faster than the bindings; more usage, and contributions, will close them over time.

Bracing for a Heavier CT Workload

The WebPKI is shifting under the feet of the CT ecosystem. Two converging trends — dramatically shorter-lived certificates and larger post-quantum keys — will pile significantly more data onto transparency logs over the next few years.

The CA/Browser Forum's Baseline Requirements currently cap publicly-trusted certificate lifetimes at 398 days. A pending ballot measure could slash that limit to as little as 47 days by March 2029. Let's Encrypt has already committed to issuing six-day certificates by the end of 2025. Crude calculations based on Merkle Town statistics suggest these changes could multiply the number of logged entries by 16-20x.

The second pressure point is cryptographic. Post-quantum certificates bring considerably larger public keys and signatures. A modern P-256 ECDSA certificate can squeeze under 1 kB; swapping in an ML-DSA44 key and signature with 96-byte UOVls-pkc SCTs balloons the same certificate to roughly 4.6 kB. That represents a 4x increase in bytes stored per log entry.

The static CT API design positions logs to absorb this load, particularly when operators distribute it across multiple log instances. Cloudflare's new implementation, available on GitHub, makes it straightforward for log operators to run their own CT logs on Cloudflare's infrastructure, injecting more operational diversity into the ecosystem. The team welcomes design and implementation feedback via GitHub issues, and encourages CAs and interested parties to begin submitting to and consuming from the test logs in the repository.