SNI (Server Name Indication)

Server Name Indication. A TLS extension that lets the client tell the server which hostname it's connecting to, before the cert is presented. Required for hosting many TLS sites on one IP.

SNI (Server Name Indication) is the TLS extension defined in RFC 6066 that carries the hostname a client wants inside the ClientHello, before any certificate is chosen. It is the reason one IP address can serve valid certificates for thousands of different domains, and the reason multi-tenant custom domains work at all.

Without it, a server listening on one IP has no way to know which certificate to present, so it can only ever hold one. RFC 6066 obsoleted the older RFC 4366, which is why half the references you find still point at the wrong document.

What actually travels in the ClientHello

The server_name extension is the first thing the client says, in the clear, before the TLS handshake has produced any keys:

  1. Client opens TCP, sends ClientHello with server_name = acme.com.
  2. Server looks up which certificate covers acme.com.
  3. Server presents that cert in its ServerHello.
  4. Handshake completes, and only then does the first HTTP request travel.

The server doing steps 2 and 3 is whatever terminates TLS for you, which on a custom-domain platform is the edge rather than your application. Step 2 is the whole point. The certificate has to be chosen before the connection is encrypted, so the hostname cannot be encrypted by the same connection it is selecting a key for. That chicken-and-egg problem is why SNI spent 20 years in plaintext, and why fixing it needed a separate mechanism (see ECH below).

RFC 6066 says clients SHOULD send server_name whenever they locate a server by name. In practice every browser and every current TLS library does.

What nine production edges do when SNI is missing

This is where documentation usually stops. We measured it instead. Each of these is a real handshake opened against the host's IP with no -servername flag, on 2026-08-19:

PlatformBehaviour with no SNIWhat comes back
Domainee (Railway)Fallback certCN=*.up.railway.app
VercelFallback certCN=no-sni.vercel-infra.com
NetlifyFallback certCN=*.netlify.app
CloudflareFallback certCN=cloudflare.com
Ghost (Fastly)Fallback certCN=fallback.tls.fastly.net
WebflowFallback certCN=*.us-east-1-prod.kube.webflow.services
HerokuHandshake abortedTLS alert 112, unrecognized_name
Cloudflare PagesHandshake abortedTLS alert 40, handshake_failure
Fly.ioConnection hangsno alert, no cert

Two things worth pulling out of that table.

Vercel mints a certificate whose only job is to name the failure. no-sni.vercel-infra.com is not a real site, and it never appears in CT logs as a hostname anyone visits. It exists so that anyone debugging a broken client sees the cause in the certificate subject rather than a generic mismatch.

Only Heroku sends the alert RFC 6066 actually specifies. The spec says a server that does not recognise the name SHOULD either abort with a fatal unrecognized_name(112) alert or carry on. Heroku aborts with 112. Cloudflare Pages aborts with a generic handshake_failure(40). Fly.io does not answer. So the alert number on a failed handshake tells you something about which edge you hit, but it will not reliably tell you that SNI was the problem.

The practical consequence: a client that omits SNI does not fail the same way everywhere. On six of these nine it gets a working TLS connection to the wrong name and fails later, at certificate validation, with ERR_CERT_COMMON_NAME_INVALID. On the other three it fails at the handshake with no certificate at all.

SNI is not the Host header, and confusing them breaks tenant routing

They carry the same string most of the time, which is exactly why this bites.

SNIHost / :authority
LayerTLSHTTP
Sentonce, per connection, in the clearonce per request, encrypted
Chooseswhich certificatewhich app, tenant, or vhost
SpecRFC 6066RFC 9110

One connection carries one SNI and many requests, each with its own Host header. On HTTP/1.1 that distinction rarely matters. On HTTP/2 it matters a lot, because of connection coalescing.

A browser is allowed to reuse an existing HTTP/2 connection for a different origin when two conditions hold: the new hostname resolves to an IP already in use, and the certificate on that connection also covers the new hostname. Both are routinely true on a multi-tenant platform that puts many customer domains behind one edge IP and one SAN certificate. When they are, the browser sends requests for tenant-b.com down the connection it opened for tenant-a.com, and the SNI on that connection still says tenant-a.com.

So: if your platform routes tenants by SNI, HTTP/2 will eventually hand the wrong tenant's data to the wrong customer. Route on the :authority or Host header. The server's escape hatch when it cannot serve a coalesced request is HTTP 421 Misdirected Request, which tells the browser to open a fresh connection and retry.

There is an architectural corollary that is easy to miss: issuing one certificate per custom hostname makes coalescing impossible, because no connection's certificate ever covers a second tenant. That is how Cloudflare SSL for SaaS and Domainee both work, and it is a security property, not just a provisioning detail. A wildcard certificate or a shared multi-domain cert across tenants trades that property away.

SNI is a hint, not authentication

Nothing verifies that the SNI you send matches the Host header you later send, or that you have any right to either. The client picks the string. All the security comes afterwards, from the client validating the X.509 chain against the name it actually wanted.

This is the mechanism behind domain fronting: send one hostname in the SNI and a different one in the Host header, and on an edge that does not check the two against each other, traffic reaches a destination the network observer never saw. Most large CDNs now reject mismatched pairs for exactly this reason. If you operate an edge, checking that the request's Host is one your connection's certificate actually covers is cheap and worth doing.

Encrypted Client Hello: SNI stopped being permanently plaintext in March 2026

Most explainers still describe ECH as a draft. It is not. Encrypted Client Hello was published as RFC 9849 in March 2026, Proposed Standard. (It is not RFC 9001, which is the QUIC-TLS binding and gets miscited constantly.) The older ESNI proposal it replaced was abandoned years earlier.

ECH solves the chicken-and-egg problem by borrowing a key from somewhere other than the connection. The server publishes an ECH config in a DNS HTTPS record; the client encrypts the real ClientHello to that key and wraps it in an outer ClientHello carrying a harmless shared public_name. You can read it out of DNS yourself:

dig -t TYPE65 crypto.cloudflare.com +short

Decoding the SvcParams from that answer today returns four keys (alpn, ipv4hint, ech, ipv6hint), and the 71-byte ech value contains the public name in cleartext: cloudflare-ech.com. Every Cloudflare-fronted site using ECH shows that same outer name on the wire, which is the entire trick. The observer learns you are talking to Cloudflare and nothing more.

Two honest limitations. ECH needs DNS over HTTPS or equivalent to be worth anything, since a plaintext DNS lookup for the real hostname leaks what ECH just hid. And adoption is one-sided: Cloudflare and Firefox and Chrome support it, but most origins publish no HTTPS record at all. domainee.dev currently does not, and neither will most of your customers' domains.

For a typical SaaS custom domain, plaintext SNI is not a threat you need to solve. The hostname was already visible in the DNS query that preceded it.

Testing SNI yourself

Two commands cover almost every SNI question:

# What cert do I get for this name?
openssl s_client -connect domainee.dev:443 -servername domainee.dev </dev/null 2>&1 | grep subject

# What does this edge do when SNI is absent?
openssl s_client -connect 69.46.46.75:443 </dev/null 2>&1 | grep -E 'subject|alert'

The second one has to connect by IP. If you pass a hostname without -servername, some builds of OpenSSL fill the SNI in for you and you end up testing nothing. If you would rather not shell out, our SSL certificate checker shows the served chain per hostname, and the CNAME lookup and DNS record lookup tell you which edge the name lands on in the first place.

Where SNI fits in a custom-domain product

Every custom-domain feature is built on the same four steps: the customer points a CNAME or A record at your edge, you provision a certificate for their hostname over ACME (usually Let's Encrypt, via the DNS-01 challenge so it works before traffic arrives), the edge selects that certificate by SNI at handshake time, and your app routes the tenant by Host header, usually through a custom domain API. SNI is step three, and it is the step nobody has to build, which is why it goes unmentioned until it breaks.

Domainee is a custom domains API for SaaS with a native MCP server — 50 domains and 100 GB free. It issues one certificate per customer hostname and renews them automatically, so the SNI path above is handled end to end. If you are weighing that against building it, the Cloudflare SSL for SaaS setup guide walks the DIY version, connecting custom domains to your SaaS covers the product-side flow, and the Cloudflare SSL for SaaS comparison puts the two side by side on price. Framework-specific wiring lives in the Next.js multi-tenant guide. Full per-domain and bandwidth rates are on pricing.

FAQ

Which RFC defines SNI? RFC 6066, which specifies server_name as a TLS extension carried in the ClientHello. It obsoleted RFC 4366, so older references pointing there are describing the same mechanism under a retired document number. RFC 6066 also defines the unrecognized_name(112) alert a server may use to reject a name it does not serve.

Is SNI encrypted? Classic SNI is not. It travels in plaintext in the ClientHello because the certificate has to be selected before any key exchange completes. Encrypted Client Hello (RFC 9849, March 2026) fixes this by encrypting the real ClientHello to a key the server publishes in a DNS HTTPS record, but it only helps when the DNS lookup is also encrypted.

What happens if a client does not send SNI? It depends entirely on the edge. Six of the nine platforms we measured return a fallback certificate, so the handshake succeeds and validation fails afterwards with a name-mismatch error. Heroku aborts with TLS alert 112, Cloudflare Pages with alert 40, and Fly.io simply does not respond.

Why does a multi-tenant SaaS need SNI? Because it is what lets one IP present a different valid certificate to every customer domain. Without SNI each custom domain would need its own IP address and its own listener, which is the arrangement SNI was introduced in 2003 (RFC 3546) to eliminate.

Can I route tenants by SNI instead of the Host header? No. HTTP/2 connection coalescing lets a browser send requests for one hostname over a connection opened for another, whenever the IPs overlap and the certificate covers both. The SNI then belongs to the wrong tenant. Route on Host or :authority, and return 421 Misdirected Request when you cannot serve a coalesced request.

Do any clients still fail to send SNI? Very few. Java 6, Python 2.7 before 2.7.9, Android 2.x, and some embedded and IoT TLS stacks omit it. All are past a decade old. The more common cause of a missing SNI in 2026 is a script or a health check connecting by raw IP address, which is exactly the case the fallback certificates in the table above exist to make legible.

Want this handled for you? Start free with Domainee — 50 custom domains + 100 GB bandwidth, no card.