A TLS handshake timeout occurs when the client and server fail to complete the SSL/TLS negotiation within the timeout period (default 120 seconds). This typically indicates network delays, server resource constraints, or infrastructure issues.
A TLS (Transport Layer Security) handshake timeout happens when the SSL/TLS connection negotiation between client and server doesn't complete within the configured timeout window. During the handshake phase, the client and server exchange cryptographic information, verify certificates, and negotiate encryption parameters. If this exchange takes longer than the timeout period, the connection is terminated with this error. The default handshake timeout in Node.js is 120 seconds (120,000 milliseconds). This error typically indicates the server is slow to respond, the network has significant latency, or intermediate infrastructure (firewalls, load balancers, proxies) is interfering with the connection.
First, confirm the server is actually listening and responding. Use curl or telnet to test basic connectivity:
# Test basic TCP connection (ignore SSL errors for now)
curl -v https://your-server.com
# Or use nc/telnet to test the port
nc -zv your-server.com 443If the server isn't responding at all, restart the Node.js application and check server logs for startup errors.
A common cause is the server being resource-constrained during the handshake. Monitor server metrics:
# On Linux
top
ps aux | grep node
# On macOS
activity monitor
# Check available memory
free -h # Linux
vm_stat # macOSIf CPU is maxed out or memory is low, the handshake processing is delayed. Scale up resources or optimize your application code.
If your server legitimately needs more time (e.g., complex certificate chains or slow networks), increase the handshake timeout in your Node.js TLS configuration:
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.cert'),
handshakeTimeout: 30000 // 30 seconds instead of default 120s
};
const server = https.createServer(options, (req, res) => {
res.writeHead(200);
res.end('OK');
});
server.listen(443);For client connections making outbound HTTPS requests:
const https = require('https');
const agent = new https.Agent({
handshakeTimeout: 30000 // Increase timeout for outbound connections
});
const options = {
hostname: 'api.example.com',
port: 443,
path: '/endpoint',
method: 'GET',
agent: agent
};
const req = https.request(options, (res) => {
console.log('statusCode:', res.statusCode);
});
req.on('error', (error) => {
console.error('Request failed:', error.message);
});Verify the network path between client and server isn't introducing excessive delays:
# Test latency to the server
ping your-server.com
mtr your-server.com # Shows hop-by-hop latency
# Check for packet loss
ping -c 100 your-server.com # Look for packet loss percentageReview firewall rules, load balancer configuration, and proxy settings:
- Ensure the firewall isn't rate-limiting or blocking SSL connections
- Check if a load balancer is configured with incompatible TLS settings
- Verify any reverse proxies (nginx, HAProxy) have appropriate timeouts
- Disable any SSL inspection or MITM proxies that might be interfering
Ensure your TLS configuration is efficient:
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.cert'),
// Modern, performant cipher suites
ciphers: 'ECDHE+AESGCM:ECDHE+AES256',
// Preferred TLS versions (drop old versions)
minVersion: 'TLSv1.2',
maxVersion: 'TLSv1.3',
// Session reuse for faster handshakes
sessionTimeout: 3600,
handshakeTimeout: 30000
};
const server = https.createServer(options, (req, res) => {
res.writeHead(200);
res.end('OK');
});
server.listen(443);Benefits of this configuration:
- Strong, modern ciphers reduce negotiation time
- TLSv1.3 is faster than TLSv1.2
- Session reuse avoids repeated full handshakes
For client code making repeated requests, reuse connections with an HTTPS agent to avoid repeated handshakes:
const https = require('https');
const agent = new https.Agent({
keepAlive: true, // Reuse connections
keepAliveMsecs: 30000, // Keep alive timeout
maxSockets: 50, // Max concurrent connections
handshakeTimeout: 30000
});
// Reuse agent across multiple requests
function makeRequest(url) {
return new Promise((resolve, reject) => {
https.get(url, { agent }, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(data));
}).on('error', reject);
});
}
// All requests reuse the same connection pool
makeRequest('https://api.example.com/endpoint1');
makeRequest('https://api.example.com/endpoint2');An invalid or incomplete certificate chain can cause handshake delays or failures:
# Inspect your certificate details
openssl x509 -in server.cert -text -noout
# Verify the certificate chain is complete
openssl s_client -connect your-server.com:443 -showcerts
# Check for certificate warnings
curl -v --insecure https://your-server.com 2>&1 | grep -i certEnsure:
- Certificate is not expired (notAfter date is in the future)
- Certificate hostname matches your domain (check Subject Alternative Names)
- Intermediate certificates are included in the cert chain
- Certificate is signed by a trusted CA
TLS Handshake Performance Optimization:
The TLS handshake involves multiple round-trips between client and server. With TLSv1.3, the handshake can be completed in a single round-trip for resumed sessions and 1-RTT for new connections. Ensure both client and server are using TLSv1.3 for best performance.
Session Resumption:
Implementing TLS session resumption (session IDs or tickets) allows clients to reconnect without a full handshake, significantly reducing reconnection time. Node.js handles this automatically if sessions are not explicitly disabled.
SNI (Server Name Indication):
If hosting multiple HTTPS domains on one IP, ensure SNI is properly configured. Misconfigured SNI can cause handshake delays or certificate mismatches:
const https = require('https');
const fs = require('fs');
const options = {
SNICallback: (servername, cb) => {
// Load appropriate cert based on servername
const cert = fs.readFileSync(`/etc/ssl/${servername}.cert`);
const key = fs.readFileSync(`/etc/ssl/${servername}.key`);
cb(null, tls.createSecureContext({ cert, key }));
}
};
const server = https.createServer(options);Monitoring Handshake Performance:
Track handshake time with server events:
const https = require('https');
const server = https.createServer(options);
server.on('secureConnection', (socket) => {
const handshakeTime = Date.now();
console.log('Connection established', socket.getPeerCertificate());
});
server.on('clientError', (err, socket) => {
if (err.code === 'ETIMEDOUT') {
console.error('Handshake timeout detected');
}
});Platform-Specific Considerations:
On Windows, TLS handshakes can be slower due to certificate validation overhead. On Linux, ensure OpenSSL is up-to-date for better TLS performance. Docker containers should have sufficient CPU allocated to avoid handshake delays.
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