Laravel SSL Handshake Failed: How to Find the Cause and Fix It
Fix "SSL handshake failed" errors in Laravel's HTTP client, mailer, MySQL and Redis connections: reproduce the failure with openssl s_client, then fix CA bundles, TLS versions, ports and SNI without turning off certificate verification.
Before any HTTPS, SMTPS or TLS database traffic flows, the client and server run a TLS handshake. The client says which protocol versions and cipher suites it supports and which hostname it wants (SNI). The server picks a version and cipher, then presents a certificate chain, which the client checks against its trusted CA bundle. If any of those steps fails, the connection is closed and PHP reports a handshake error.
When Laravel is the client, as with Http::get(), sending mail or connecting to a managed database, the handshake uses the OpenSSL library and CA bundle on the server running PHP, not your browser's. That is why a URL can open fine in Chrome and still fail from your app, a queue worker or a Docker container.
The error text usually tells you which step failed:
| Error | What it usually means |
|---|---|
cURL error 35: OpenSSL SSL_connect: SSL_ERROR_SYSCALL |
The connection was dropped mid-handshake: a firewall or proxy, the wrong port, or a server that rejects clients without SNI |
error:0A000410:SSL routines::sslv3 alert handshake failure |
No protocol version or cipher suite in common between client and server |
error:0A00010B:SSL routines::wrong version number |
TLS spoken to a plaintext port, or the reverse. Common with mail ports |
error:0A000152:SSL routines::unsafe legacy renegotiation disabled |
The remote server is too old for OpenSSL 3's defaults |
cURL error 60: SSL certificate problem: unable to get local issuer certificate |
The handshake reached certificate checking, but the CA bundle cannot verify the chain |
The tempting fix is to switch verification off with Http::withoutVerifying(), 'verify' => false or verify_peer => false. Do not ship that. It keeps the traffic encrypted but stops checking who you are talking to, so anyone who can intercept the connection can impersonate the API, mail server or database and read everything you send it, including API keys and passwords. Every cause in this guide has a fix that keeps verification on.
The Problem
An SSL handshake failure means your Laravel application and the server it is connecting to could not agree on how to set up an encrypted connection, so the connection was dropped before any data was sent. In Laravel it usually shows up as cURL error 35 from the HTTP client, a "wrong version number" or STARTTLS error from the mailer, or an SSL error from a MySQL or Redis connection.
How to Fix
-
1
Reproduce the handshake outside Laravel
Run the handshake from the same server or container that runs Laravel, so you test the same OpenSSL build and CA bundle:
openssl s_client -connect api.example.com:443 -servername api.example.comForce a protocol version to see which ones the remote side accepts:
openssl s_client -connect api.example.com:443 -servername api.example.com -tls1_2 openssl s_client -connect api.example.com:443 -servername api.example.com -tls1_3Check which OpenSSL and cURL builds PHP itself is using, since the CLI and PHP-FPM can differ:
php -r 'echo OPENSSL_VERSION_TEXT, PHP_EOL, curl_version()["ssl_version"], PHP_EOL;'If openssl s_client also breaks, the problem is the remote server, the network or protocol support, not your Laravel code. If it succeeds but Laravel does not, compare the hostname, port and CA bundle in your Laravel config.
-
2
Update the CA bundle PHP verifies against
If the error mentions "certificate verify failed" or "unable to get local issuer certificate", PHP cannot find a trusted CA that signed the remote certificate. Update the system bundle:
sudo apt install --reinstall ca-certificatesSlim Docker images often ship without it, so install it in the image itself:
# Dockerfile RUN apt-get update && apt-get install -y ca-certificatesCheck whether php.ini points PHP at a different, older bundle:
php -i | grep -E "openssl.cafile|curl.cainfo"If either is set, point both at the system bundle (/etc/ssl/certs/ca-certificates.crt on Debian and Ubuntu) or remove them. If openssl s_client shows "verify error:num=20" against a public API, the remote server may be missing its intermediate certificate. That is the provider's bug to fix. Report it rather than disabling verification.
For a private CA, such as an internal service, trust that one CA for that one client:
Http::withOptions(['verify' => storage_path('certs/internal-ca.pem')])->get($url); -
3
Fix protocol, cipher and SNI mismatches
"sslv3 alert handshake failure" means the two sides share no protocol version or cipher suite. Check what a working client negotiates:
curl -sv https://api.example.com -o /dev/null 2>&1 | grep -E "SSL connection|TLSv"OpenSSL 3, the version on Ubuntu 22.04 and later, rejects TLS 1.0, TLS 1.1 and legacy renegotiation by default. If the remote server only speaks those, the fix belongs on that server. Lowering your OpenSSL security level re-enables broken protocol versions for every connection your app makes.
Connect by hostname, not IP address. A request to an IP sends no SNI, and servers behind Cloudflare or a shared load balancer then cannot pick a certificate and drop the handshake. The same applies to your database and Redis host settings.
If the failure only happens on one network, look for a corporate proxy or firewall doing TLS inspection. It replaces the remote certificate with its own, which your server does not trust.
-
4
Match the mail port to the right scheme
"wrong version number" from the mailer almost always means TLS on a plaintext port or the reverse. Port 465 expects TLS from the first byte (smtps). Port 587 starts in plaintext and upgrades with STARTTLS (smtp):
MAIL_PORT=465 MAIL_SCHEME=smtpsMAIL_PORT=587 MAIL_SCHEME=smtpIn Laravel 11 and later, an empty scheme setting makes Laravel pick smtps for port 465 and smtp for every other port, so the usual mistake is an explicit smtps scheme left over on port 587. Laravel 10 and earlier configure this through the encryption setting instead. Symfony Mailer upgrades port 587 connections with STARTTLS whenever the server offers it, so no extra setting is needed for that.
-
5
Configure TLS for MySQL and Redis
Managed databases from AWS, DigitalOcean, Aiven and others usually require TLS and sign their certificates with their own CA. Download the provider's CA file and point MySQL at it. The default config/database.php already reads this variable:
DB_HOST=db.example.com MYSQL_ATTR_SSL_CA=/etc/ssl/certs/provider-ca.pemFor managed Redis, switch to a rediss:// URL. Laravel maps the rediss scheme to TLS:
REDIS_URL=rediss://default:your-password@redis.example.com:6380Use the hostname from your provider's dashboard, not a private IP, so the certificate matches.
How to Verify
Re-run the failing call from the same server, as the same user that runs PHP-FPM or your queue workers:
php artisan tinker
Http::get('https://api.example.com')->status();
For mail, send a test message from tinker and watch the log for the TLS error:
Mail::raw('TLS test', fn ($message) => $message->to('you@example.com'));
Then check that nobody "fixed" the error by disabling verification:
grep -rn "withoutVerifying\|'verify' => false\|verify_peer" app config
Prevention
Keep the base image and the ca-certificates package updated, connect to every service by hostname, and set the mail port and MAIL_SCHEME together. Keep Http::withoutVerifying() out of production code and flag it in code review. StackShield monitors your own certificate and TLS configuration, so handshake failures caused by your server show up before your users report them.
Frequently Asked Questions
What does "SSL handshake failed" mean in Laravel?
It means Laravel and the server it was connecting to could not agree on how to set up an encrypted connection, so the connection was closed before any data was sent. The cause is usually an outdated CA bundle, a protocol or cipher mismatch, a mail port configured with the wrong scheme, or a connection made by IP address without SNI.
Is it safe to use Http::withoutVerifying() to fix SSL errors?
No. It keeps the connection encrypted but stops Laravel checking the server certificate, so anyone who can intercept the traffic can impersonate the API and read the credentials you send it. Fix the CA bundle, or trust a specific private CA with the verify option, instead.
Why does the URL work in my browser but fail in Laravel?
Your browser uses its own certificate store and can fetch missing intermediate certificates on its own. Laravel uses the OpenSSL library and CA bundle on the server running PHP, which may be older, missing in a slim Docker image, or behind a proxy that inspects TLS. Test from that server with openssl s_client to see what PHP sees.
What is cURL error 35 in Laravel?
cURL error 35 is a generic SSL connect error raised by the HTTP client when the TLS handshake fails. The OpenSSL message after it, such as SSL_ERROR_SYSCALL or "sslv3 alert handshake failure", tells you whether the connection was dropped or the two sides had no protocol or cipher in common.
Related Guides
How to Fix Weak SSL/TLS Configuration in Laravel
Your SSL/TLS certificate is expired, misconfigured, or using weak protocols. Learn how to fix SSL issues for your Laravel app.
How to Fix Missing Email Security Records (SPF/DKIM/DMARC)
Missing SPF, DKIM, and DMARC records allow attackers to send spoofed emails from your domain. Configure email authentication now.
How to Fix DNS Security Issues and Misconfigurations
DNS misconfigurations like open zone transfers and missing DNSSEC expose your domain to hijacking and spoofing attacks. Fix them now.
Is your Laravel app exposed right now?
34% of Laravel apps we scan have at least one critical issue, and most teams do not find out until something breaks. The free scan checks your live app in 60 seconds. Then StackShield re-runs every check after each deploy, so a fix you ship today does not quietly regress next week.