This error occurs when a Node.js client and server cannot establish a secure TLS/SSL connection during the handshake process. The handshake failure typically stems from protocol version mismatches, incompatible cipher suites, certificate validation issues, or misconfigured TLS settings.
The SSL_ERROR_BAD_HANDSHAKE error indicates that Node.js failed to complete the TLS/SSL handshake process when establishing a secure connection. The TLS handshake is a multi-step negotiation where the client and server agree on protocol versions, cipher suites, exchange certificates, and generate encryption keys to create a secure session. When this handshake fails, it means the client and server were unable to find a mutually compatible configuration. This could be due to protocol version incompatibilities (e.g., one side only supports TLS 1.0 while the other requires TLS 1.2+), cipher suite mismatches where no common encryption algorithm exists, certificate validation failures, or network-level issues interrupting the handshake sequence. Node.js has strict security defaults that prefer modern protocols (TLS 1.2+) and strong cipher suites with perfect forward secrecy. While this improves security, it can cause handshake failures when connecting to servers with outdated configurations or when using self-signed certificates without proper configuration.
Use OpenSSL to test the connection and see what protocols and ciphers the server supports:
# Test TLS connection and view certificate details
openssl s_client -connect example.com:443 -servername example.com
# Check supported TLS versions
openssl s_client -connect example.com:443 -tls1_2
openssl s_client -connect example.com:443 -tls1_3
# List supported cipher suites
openssl s_client -connect example.com:443 -cipher 'ALL' -servername example.comLook for protocol version, cipher suite details, and any certificate validation errors in the output.
Older Node.js versions may lack support for modern TLS protocols and trusted Certificate Authorities:
# Check your current Node.js version
node --version
# Update using nvm (recommended)
nvm install --lts
nvm use --lts
# Or download from nodejs.org
# https://nodejs.org/After updating, test your connection again. Many handshake issues are resolved by using a current Node.js version.
If connecting to a server with self-signed or custom CA certificates, provide the CA certificate to Node.js:
const https = require('https');
const fs = require('fs');
const options = {
hostname: 'example.com',
port: 443,
path: '/',
method: 'GET',
ca: fs.readFileSync('/path/to/ca-cert.pem'),
// Optionally specify minimum TLS version
minVersion: 'TLSv1.2',
};
const req = https.request(options, (res) => {
console.log('Status:', res.statusCode);
res.on('data', (d) => process.stdout.write(d));
});
req.on('error', (error) => {
console.error('Error:', error);
});
req.end();For Axios or other HTTP clients:
const axios = require('axios');
const https = require('https');
const fs = require('fs');
const agent = new https.Agent({
ca: fs.readFileSync('/path/to/ca-cert.pem'),
minVersion: 'TLSv1.2',
});
axios.get('https://example.com', { httpsAgent: agent })
.then(response => console.log(response.data))
.catch(error => console.error(error));If the server supports modern TLS but Node.js is trying older protocols, explicitly set the minimum version:
const https = require('https');
const tls = require('tls');
const options = {
hostname: 'example.com',
port: 443,
path: '/',
method: 'GET',
minVersion: 'TLSv1.2',
maxVersion: 'TLSv1.3',
};
https.request(options, (res) => {
console.log('Connected with:', res.socket.getProtocol());
res.on('data', (d) => process.stdout.write(d));
}).end();You can also set this globally:
const tls = require('tls');
tls.DEFAULT_MIN_VERSION = 'TLSv1.2';If you need to support specific cipher suites or connect to servers with limited cipher support:
const https = require('https');
const options = {
hostname: 'example.com',
port: 443,
path: '/',
method: 'GET',
ciphers: 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384',
honorCipherOrder: true,
};
https.request(options, (res) => {
console.log('Cipher:', res.socket.getCipher());
res.on('data', (d) => process.stdout.write(d));
}).end();To see Node.js default cipher list:
node -e "console.log(require('tls').DEFAULT_CIPHERS)"Warning: Only modify cipher suites if absolutely necessary. Node.js defaults are secure and well-tested.
Certificate validity is time-sensitive. Incorrect system time can cause handshake failures:
# Check current system time
date
# On Linux, sync with NTP
sudo ntpdate -s time.nist.gov
# Or use systemd-timesyncd
sudo timedatectl set-ntp true
timedatectl status
# On macOS
sudo sntp -sS time.apple.comAfter correcting the time, retry your Node.js connection.
Node.js can use the operating system's certificate store instead of its bundled certificates:
# Run Node.js with system CA certificates
node --use-openssl-ca app.js
# Or set environment variable
export NODE_OPTIONS="--use-openssl-ca"
node app.jsThis can help when the server certificate is trusted by your OS but not by Node.js's bundled CA list.
Get verbose TLS handshake information to diagnose the exact failure point:
# Enable Node.js TLS debugging
NODE_DEBUG=tls node app.js
# Or use environment variable in code
process.env.NODE_DEBUG = 'tls';This will output detailed information about:
- Protocol negotiation
- Cipher suite selection
- Certificate exchange
- Exact point of handshake failure
Use this output to identify whether the issue is protocol mismatch, cipher incompatibility, or certificate validation.
Security Considerations: Never disable certificate validation in production by setting rejectUnauthorized: false or NODE_TLS_REJECT_UNAUTHORIZED=0. This completely undermines the security provided by TLS and makes your application vulnerable to man-in-the-middle attacks. If you need to accept self-signed certificates, always use the ca option to explicitly trust specific certificates.
Protocol Evolution: TLS 1.0 and 1.1 are deprecated as of 2020 and disabled by default in modern Node.js versions (12+). If you must connect to legacy servers, you can set minVersion: 'TLSv1', but be aware of the security implications. Consider upgrading the server instead.
SNI (Server Name Indication): When connecting via HTTPS, Node.js automatically sends the SNI extension with the hostname. For IP-based connections, you may need to manually specify the servername in options: servername: 'example.com'. This is crucial for servers hosting multiple SSL certificates.
Perfect Forward Secrecy: Node.js prefers cipher suites with PFS (ECDHE, DHE) which ensure that past sessions remain secure even if the server's private key is later compromised. Some legacy systems don't support PFS ciphers, causing handshake failures.
Certificate Chain Issues: If the server doesn't send the complete certificate chain (intermediate certificates), Node.js may fail to validate the certificate. The server administrator needs to configure the full chain. You cannot fix this client-side without disabling validation.
Docker and Container Issues: In containerized environments, ensure CA certificates are present in the container image. Alpine-based images may need: apk add --no-cache ca-certificates. Also verify that the container's system time is synchronized with the host.
Proxy and Corporate Environments: Corporate proxies often perform TLS inspection (MITM), which requires installing the proxy's CA certificate. Use the ca option or system CA store to trust the proxy certificate.
HTTP/2 and ALPN: When using HTTP/2 over TLS, ensure both client and server support ALPN (Application-Layer Protocol Negotiation). Node.js supports this automatically, but some older servers may not.
Error: EMFILE: too many open files, watch
EMFILE: fs.watch() limit exceeded
Error: Middleware next() called multiple times (next() invoked twice)
Express middleware next() called multiple times
Error: Worker failed to initialize (worker startup error)
Worker failed to initialize in Node.js
Error: EMFILE: too many open files, open 'file.txt'
EMFILE: too many open files
Error: cluster.fork() failed (cannot create child process)
cluster.fork() failed - Cannot create child process