A site loads perfectly in a browser and your script refuses it with a certificate error. Nothing is inconsistent. The two verify differently.
Why they disagree
A browser carries the operating system's trust list, kept reasonably current, and it caches intermediate certificates it has encountered on other sites. So it can often complete a chain the server failed to send.
A script carries its own trust list, frequently a bundle shipped with the language, the package or the container image, and frequently old. It caches nothing. An incomplete chain fails, every time, on every machine.
That single difference explains most of these reports.
Diagnose in one command
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | head -20
Look for the verification result. Verify return code: 0 (ok) means the chain is complete from this machine's point of view.
unable to get local issuer certificate means it is not, either the server is not sending the intermediates, or this machine does not have the root.
Distinguish them by testing from a machine you know is current. If that succeeds and the failing one does not, it is the client's trust list; if both fail, it is the server. Checking a certificate from the command line explains reading the output.
Cause one: the server is not sending intermediates
The most common, and it is your problem to fix if the server is yours.
The server must send its certificate and the intermediates, not the root, which the client already has. Understanding the certificate chain goes into installing the full chain, and why a certificate is trusted walks through why the missing piece matters.
If the server is somebody else's, tell them. It is a real misconfiguration affecting every non-browser client, not just yours.
Cause two: the client's trust list is old
Containers and older systems ship a bundle that stops being updated. When an authority introduces a new root, that client cannot verify certificates under it.
apt-get update && apt-get install --reinstall ca-certificates # Debian/Ubuntu yum reinstall ca-certificates # RHEL family
In PHP, the bundle used is a configuration setting and is frequently unset or pointing at a file that no longer exists:
php -i | grep -i cafile
Point it at the system bundle rather than shipping your own copy, which becomes the next stale bundle.
Cause three: the server name is not being sent
One address hosts many sites, and the server chooses the certificate from the name requested.
A client that does not send it gets the server's default certificate, which is for a different site, and the mismatch is reported as a certificate error.
Modern clients send it automatically. Very old libraries, and some hand-rolled connections, do not, and the symptom is a certificate error naming a hostname you have never heard of.
What never to do
curl -k https://example.com # do not CURLOPT_SSL_VERIFYPEER => false # do not
These make the error disappear by removing verification entirely. The connection is still encrypted and there is no longer anything establishing who it is encrypted to, so any intermediary can present their own certificate and read everything.
That matters especially here, because these connections usually carry API keys and credentials, which is exactly what an intermediary would want.
It appears constantly in forum answers because it works instantly. It is the difference between fixing the fault and removing the alarm.
The legitimate exception
A private service using an internal authority, where the correct answer is to add that authority's root to the client's trust list rather than disable verification:
curl --cacert /path/to/internal-ca.crt https://internal.example.com
Verification still happens; you have simply told the client who else to trust. Using client certificates walks through the arrangement where you run your own authority.
When it worked yesterday
Three usual causes. The certificate renewed and the new chain is not installed correctly. An authority changed its intermediates. Or the server's clock is wrong, so a valid certificate appears not yet valid. There is more on that one, and it is the least suspected in why a server clock matters.
Find which bundle the client is actually using
Each language and runtime has its own idea of where trusted roots live, and the failing one is frequently not the system's.
openssl version -d
python3 -c "import ssl; print(ssl.get_default_verify_paths())"
node -p "process.env.NODE_EXTRA_CA_CERTS || 'system default'"
php -r 'echo ini_get("openssl.cafile") ?: "unset", "\n";'
A path that does not exist, or one inside an application directory instead of the system's, is the finding. Applications that ship their own bundle keep whatever was current when they were packaged.
Pointing the client at the system bundle instead of a private copy means it is maintained by the operating system's updates rather than by nobody, which is the difference between a fix and the same problem in two years.
Containers are the common case now
A minimal container image frequently ships without any certificate bundle at all, and every outbound HTTPS request fails with a verification error that reads like a server problem.
ls -la /etc/ssl/certs/ca-certificates.crt 2>/dev/null apk add --no-cache ca-certificates # Alpine apt-get install -y ca-certificates # Debian
The signature is distinctive: the same code works on a developer's machine and fails identically for every destination inside the container. That is not a certificate problem with any particular server. It is the absence of the list.
Installing the package is the fix, and it belongs in the image instead of being applied at runtime. For the build, see running Docker on a VPS.
Verify against a known-good destination first
Before investigating a specific server, establish whether the client can verify anything at all.
curl -sI https://example.com >/dev/null && echo "client verification works" openssl s_client -connect example.com:443 </dev/null 2>/dev/null | grep 'Verify return'
If a well-known site also fails, the problem is the client and no amount of work on the target server helps. If it succeeds, the problem is specific to that destination.
That single check saves the most common wasted effort here: reconfiguring a server whose chain was fine for a client that could not verify anything.
Let it fail loudly rather than quietly
The temptation under time pressure is to catch the verification error and continue, which is disabling verification with extra steps.
An integration that silently falls back to an unverified connection is worse than one that fails, because nobody investigates a working system, and the fallback becomes permanent.
Where a connection genuinely must proceed, log the failure at a level somebody sees and record how often it happens. A count that is not zero is a fault waiting to be understood in place of a setting. Why a certificate is trusted explains what the verification was establishing.