One Server Per Customer Domain Doesn't Survive Contact With Scale
Per-customer TLS breaks past a few hundred domains. How a shared edge resolves millions of custom hostnames: the registration payload, priority rules, and the RFC forcing an apex workaround.
One Server Per Customer Domain Doesn't Survive Contact With Scale
The obvious way to let customers use their own domain is one server, one certificate, per customer. It works until it doesn't, and the failure point is exactly where Shopify's six million customer domains would have broken it. Here's the routing mechanism that replaces it, with the exact registration payload, the priority rules that resolve conflicts, and the RFC that forces a workaround at the domain apex.

Say you're building a platform where customers run a store, a blog, or an app, and you want them to use their own domain, shop.customer.com, not a subdomain of yours. The default architecture is one server and one TLS certificate per customer. It's simple to reason about, isolates blast radius cleanly, and it is the wrong design past a few hundred customers, for a reason that has nothing to do with server capacity.
Shopify runs roughly six million customer domains through a shared alternative to that model, provisioning certificates "across the global network in minutes," according to its own case study with Cloudflare (Shopify case study). Webflow, BigCommerce, HubSpot, Kinsta, Render, Salesforce Commerce Cloud, and WP Engine are named in Cloudflare's provider guides as running on the same mechanism (provider guides). Cloudflare's name for it is Cloudflare for SaaS; the unit it operates on is a custom hostname. This post is about the mechanism itself, not the brand name.
Problem #1: Certificate lifecycle doesn't scale linearly (Maintainability)
One server, one cert, per customer means one renewal job per customer. Public CA certificates expire on short cycles by design, Cloudflare's own edge certificates run 90 days, and a renewal that silently fails is an outage with your name on it, discovered by the customer, not by you.
At ten customers this is a cron job you don't think about. At six million, it's six million independent points of renewal failure, and the failure mode isn't proportional to your engineering headcount, it's proportional to your customer count. That's the actual scaling limit: not compute, not bandwidth, but the number of independent things that can silently expire.
Problem #2: A shared edge has to resolve routing ambiguity, not just accept traffic (Correctness)
Replacing per-customer servers with one shared edge only works if that edge can tell, for every incoming request, which customer it belongs to and where to send it. That's not a detail, it's the entire mechanism. Cloudflare's implementation is a registration call and a lookup table.
You register a domain with a POST to /zones/{zone_id}/custom_hostnames (API reference):
{
"hostname": "shop.customer.com",
"ssl": { "method": "http", "type": "dv" }
}From that point, Cloudflare holds a mapping: "if a request arrives addressed to this hostname, it belongs to this platform, present this certificate, route to this origin." When shop.customer.com resolves to Cloudflare's shared edge, the same edge serving millions of other domains, the edge checks that table before it does anything else. No table entry, no route, regardless of DNS.
The one place this gets genuinely ambiguous: what happens when two zones both plausibly own the same hostname. Cloudflare's answer is a documented, ordered priority list, not a first-come heuristic (hostname priority):
- An exact-match custom hostname beats a wildcard custom hostname.
- A hostname that exists as its own standalone zone on Cloudflare beats a custom hostname claiming the same name, exact or wildcard, every time.
- Among exact matches, a new-style custom hostname beats a legacy one, which beats a plain DNS record in the logical zone.
That second rule is the one that produces confusing support tickets: a platform can register a wildcard custom hostname for *.customer.com, do everything correctly, and still lose routing priority the moment the customer independently onboards blog.customer.com as its own zone on Cloudflare, with zero record of it on the platform's side. The routing table isn't wrong in that scenario. It's honoring a more specific claim it was never told about.
Problem #3: Proving the customer actually owns the domain (Security)
A routing table that accepts any hostname claim is a hijacking vector, someone registers a domain they don't control and intercepts its traffic. So most platforms add a domain-control-validation, DCV, step before a certificate is ever issued.
Notion's implementation is a clean, minimal example: it requires a _notion-dcv TXT record alongside the CNAME before it will provision anything for that domain (Notion custom domains). The TXT record proves DNS control, which is a reasonable proxy for domain ownership, and it's the same class of check Cloudflare's own ssl.method field encodes: http, txt, or email validation, chosen by the platform issuing the hostname, not the customer.
Problem #4: The apex can't legally hold a CNAME (Standards compliance)
A subdomain like shop.customer.com accepts a CNAME with no issue. The bare apex, customer.com, usually can't, and the constraint is written into the DNS standard itself. RFC 1034 states that once a name carries a CNAME record, it "can't have any other entries associated with it." A domain's apex is required to carry an SOA record and NS records, and usually MX records for email. A CNAME can't legally coexist with any of them (RFC 1034; Cloudflare on CNAME flattening).
| Setup | What the customer adds | Constraint it works around |
|---|---|---|
Subdomain (shop.customer.com) | One CNAME to the platform's target | None; this is the RFC-compliant case |
Apex, Cloudflare-hosted (customer.com) | Same-looking CNAME, flattened by Cloudflare | Cloudflare recurses the chain and returns the final IP, so it's RFC-legal at the wire level |
| Apex, other DNS providers | ALIAS or ANAME record | Same trick, different vendor name |
| Apex, static-IP platforms (e.g. Vercel) | A record | No CNAME needed if the platform's IP never changes |
| Apex, subdomain-only platforms (e.g. Notion) | Not supported | Sidesteps the problem by refusing apex domains entirely |
Heroku's docs state the underlying constraint about as plainly as any vendor has: "A records aren't sufficient for pointing your root domains to Heroku because they require a static IP... Because Heroku uses dynamic IP addresses, it's necessary to use a CNAME-like record" (Heroku custom domains). Four different platforms, four different workarounds for the same RFC constraint. There's no version of this feature that skips the apex problem; there's only a choice of which workaround you inherit.
Counterargument: doesn't a shared edge concentrate risk?
Yes, and it's worth naming instead of glossing over. Millions of unrelated domains routing through one piece of infrastructure means a misconfiguration or an outage in that layer has a blast radius no single-tenant architecture would ever have. That's a real cost, not a hypothetical one.
The trade that makes it worth paying: the alternative isn't "no shared risk," it's "the same class of risk, distributed across every customer's individually-managed certificate lifecycle instead," which fails more often, more silently, and scales with headcount you don't have. Concentrating the failure mode is also what makes it fixable in one place instead of six million places.
What actually decides which architecture you need
Build the shared-edge version when the number of customer domains will exceed what a human team can individually monitor, when "my custom domain doesn't work" needs to be debuggable from one dashboard, and when the platform, not the customer, needs to own certificate renewal risk. Stick with per-customer provisioning when the customer count is small, stable, and each one already has dedicated infrastructure for other reasons.
There's a fourth failure mode worth flagging before it costs you a debugging session: everything above assumes the routing table's decision is the only thing standing between a request and the origin server. It isn't. The next post covers the second, more counterintuitive requirement, the connection from the edge to the origin has to route back through the same network it just accepted traffic from, and skipping it produces one of the more confusing error codes in the whole system.
Part 1 of a 4-part series on Cloudflare for SaaS and multi-tenant custom-domain architecture. Sources: Cloudflare for SaaS docs, Custom Hostnames API, Shopify case study, hostname priority, CNAME flattening, Heroku custom domains, RFC 1034.