Technical guide
25

Technical guide · Published Sep 4, 2025

Fix Cloudflare 525 Errors with Fly.io

Cloudflare 525 errors on Fly.io usually mean the origin certificate is wrong. Fix the custom domain cert before blaming DNS.

A field-guide drawing of a brass trail compass
In this article3 sections

My Fly.io app worked differently depending on one Cloudflare toggle. With the proxy on, it returned Error 525 (SSL handshake failed). With the proxy off, the browser showed a certificate mismatch. The problem was stale DNS preventing Fly.io from issuing the right origin certificate.

Two TLS Hops, One Failure Point

When Cloudflare proxying is enabled (orange cloud), two TLS connections exist: browser to Cloudflare edge, then Cloudflare edge to Fly.io origin. Fly.io must present a valid certificate for the custom domain. If the origin cert is missing or wrong, Cloudflare cannot complete the handshake. You get 525.

The symptom: 525 SSL handshake failed only when Cloudflare proxy was enabled. With proxy off (gray cloud / DNS only), the site showed certificate mismatch errors instead.

Warning Root cause: DNS records at Cloudflare were inconsistent (stale A/AAAA or CNAME remnants). Fly.io manages cert issuance itself through ACME (Let’s Encrypt). Dirty DNS prevented ACME validation, which meant no valid origin cert, which meant Cloudflare’s handshake failed.

01BrowserVisitor request
TLS hop 1Cloudflare edge certificate
02CloudflareProxy and validation
TLS hop 2525 happens here
03Fly.ioMust present the custom-domain certificate
The browser-facing certificate can be healthy while the origin handshake fails. A 525 means Cloudflare could not establish the second TLS connection to Fly.io.

If Cloudflare cannot finish that second TLS hop, you get 525.

The Fix

Clean your DNS. Remove every unrelated or legacy record for the apex and www. Delete old A, AAAA, and CNAME records not belonging to the Fly.io deployment. ACME validation and routing depend on unambiguous records.

Add correct AAAA records. Fly.io provides an IPv6 that routes to the app:

NameTypeContentProxy status
example.comAAAA<fly-io-ipv6-address>Proxied
www.example.comAAAA<fly-io-ipv6-address>Proxied

No A record needed if Fly.io only gave IPv6. Cloudflare terminates at edge (dual stack) then connects over IPv6 downstream.

Preserve ACME records. Leave existing _acme-challenge CNAMEs intact. These allow Let’s Encrypt to validate via DNS-01.

Wait for propagation. TTL and resolver caches need a few minutes. Verify with dig from several public resolvers.

Important Let Fly.io re-issue certificates: Once DNS points correctly, Fly.io’s dashboard shows domain status as Verified with RSA + ECDSA issued. Set Cloudflare SSL Mode to Full (Strict), not Flexible. Strict ensures Cloudflare validates the origin cert chain instead of ignoring it.

Three Things to Check

I only needed to confirm three things:

LayerHealthy stateFailure signal
DNSOnly the Fly.io records remain for apex and wwwStale A, AAAA, or CNAME records point elsewhere
Fly.ioDomain is verified and RSA + ECDSA certificates are issuedCertificate remains pending or the hostname mismatches
CloudflareProxy is on and SSL mode is Full (Strict)Flexible mode hides the origin problem

Manual probing and deep TLS inspection were noise once those were correct.

Why custom cert upload failed: Fly.io issues and renews its own certs via ACME. Cloudflare Origin Certificates only handle Cloudflare-to-origin trust and are not public-PKI certs. Uploading was never the path.

Common pitfalls: Stale A/AAAA/CNAME records lingering. Mixing previous host IPs with Fly.io AAAA. Using “Flexible” SSL (masks the real problem). Trying to import certificates instead of letting automation run.

Minimal checklist:

  1. Add domain in Fly.io
  2. Publish ONLY the Fly.io AAAA (and A if given) in Cloudflare; proxy on
  3. Leave _acme-challenge CNAMEs untouched
  4. Set SSL/TLS = Full (Strict)
  5. Wait a few minutes; confirm Fly.io shows “Verified / Issued”

Once those three checks were green, the 525 disappeared. Fly.io could issue the certificate and Cloudflare could validate it in Full (Strict) mode.

TLS proxy mistakes show up whenever you put Cloudflare in front of an origin — stack context in static blog on Cloudflare.

One quick signal

Did this earn your time?

What was missing?

Thanks. That gives me something concrete to check.