How to Solve TLS Fingerprinting in n8n with CapSolver

Ethan Collins
Pattern Recognition Specialist
18-Mar-2026

If you've ever tried to scrape a website protected by enterprise-grade bot detection, you've probably hit an invisible wall: your requests get blocked even though your headers, cookies, and User-Agent are perfect. The reason? TLS fingerprinting โ and it happens before your HTTP request is even sent.
Anti-bot services like Cloudflare, Akamai, DataDome, and others inspect the raw TLS handshake to determine whether the client is a real browser or an automation tool. Standard HTTP clients โ Go's net/http, Python's requests, curl, Node.js axios โ all have distinct TLS fingerprints that get flagged immediately.
In this guide, you'll build a lightweight Go server using httpcloak that spoofs a real Chrome TLS fingerprint, and connect it to your n8n workflows so every HTTP request looks like genuine Chrome browser traffic at the network level.
What is TLS Fingerprinting?
Every time a client connects to a website over HTTPS, it initiates a TLS handshake by sending a ClientHello message. This message contains:
- Cipher suites โ the encryption algorithms the client supports, and their order
- TLS extensions โ features like SNI, ALPN, supported groups, signature algorithms
- Elliptic curves and point formats โ cryptographic parameters
- TLS version โ the maximum TLS version supported
Anti-bot services extract these values and compute a fingerprint โ called a JA3 or JA4 fingerprint โ that uniquely identifies the client software. Every browser, HTTP library, and programming language runtime produces a different fingerprint.
| Client | JA3 Fingerprint | Detected As |
|---|---|---|
| Chrome 145 | Unique hash matching Chrome's cipher suite ordering | Real browser |
| Firefox 130 | Different hash โ Firefox uses different cipher preferences | Real browser |
Go net/http |
Completely different hash โ Go's TLS stack is obvious | Bot / automation tool |
Python requests |
Another distinct hash โ Python's urllib3 TLS is identifiable |
Bot / automation tool |
| curl | Yet another hash โ curl's TLS fingerprint is well-known | Bot / automation tool |
Node.js axios |
Node.js TLS fingerprint โ easily flagged | Bot / automation tool |
The key insight: TLS fingerprinting happens during the handshake, before any HTTP headers are sent. No amount of header manipulation can fix a non-browser TLS fingerprint.
Why Standard HTTP Clients Fail
When a browser connects to a website over HTTPS, it sends a TLS ClientHello that includes details about its supported cipher suites, extensions, and settings. Anti-bot services record this fingerprint (called a JA3 or JA4 fingerprint) and compare it to known browser profiles.
Go's net/http, Python's requests, curl, and most HTTP libraries all have distinct TLS fingerprints. Even with correct cookies and headers, anti-bot systems will block the request if they detect a non-browser TLS fingerprint.
Here's what happens step by step:
- Your n8n workflow sends an HTTP request to a protected website
- The TLS handshake begins โ your client sends its
ClientHello - The anti-bot service records the JA3/JA4 fingerprint from the handshake
- The fingerprint matches Go/Python/Node.js โ not Chrome or Firefox
- The request is blocked, challenged, or served a decoy page โ before your headers are even evaluated
This is why setting User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... doesn't help. The User-Agent is an HTTP-level header. TLS fingerprinting operates at a lower layer. If your User-Agent says Chrome but your TLS fingerprint says Go, the request is immediately flagged.
Who Uses TLS Fingerprinting?
TLS fingerprinting has become standard practice in enterprise bot protection. Here are the major services that check TLS fingerprints:
| Anti-Bot Service | TLS Check | Notes |
|---|---|---|
| Cloudflare Bot Management | Yes | Full-page "Verifying your browser..." challenge. Checks JA3/JA4 on every request |
| Akamai Bot Manager | Yes | Uses TLS fingerprinting as one of many signals in bot scoring |
| DataDome | Yes | Analyzes TLS fingerprint alongside behavioral signals |
| Many others | Varies | TLS fingerprinting is becoming standard in enterprise bot protection |
CapSolver supports solving challenges from many of these services. The TLS server in this guide is designed to work alongside any captcha-solving workflow where the final HTTP fetch needs to look like a real browser โ whether you're bypassing Cloudflare Challenge, Akamai, DataDome, or any other anti-bot system.
Prerequisites
| Requirement | Notes |
|---|---|
| n8n self-hosted | Required โ the TLS server must run on the same machine as n8n. n8n Cloud is not suitable. |
| Go 1.21+ | Must be installed on the server. Check with go version. |
| Process manager (recommended) | Any process manager (systemd, supervisor, Docker, PM2) to keep the TLS server running across reboots |
Step 1 โ Build the TLS Server
The TLS server is a lightweight Go HTTP server that accepts requests on port 7878 and forwards them using httpcloak's Chrome-145 TLS preset.
Create the source file
bash
mkdir -p ~/tls-server && cd ~/tls-server
Create a file called main.go with the following content:
go
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
"github.com/sardanioss/httpcloak/client"
)
type FetchRequest struct {
URL string `json:"url"`
Method string `json:"method"`
Headers map[string]string `json:"headers"`
Proxy string `json:"proxy"`
Body string `json:"body"`
}
type FetchResponse struct {
Status int `json:"status"`
Body string `json:"body"`
Headers map[string][]string `json:"headers"`
}
type ErrorResponse struct {
Error string `json:"error"`
}
func writeError(w http.ResponseWriter, status int, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(ErrorResponse{Error: msg})
}
func fetchHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "only POST allowed")
return
}
var req FetchRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
return
}
if req.URL == "" {
writeError(w, http.StatusBadRequest, "url is required")
return
}
if req.Method == "" {
req.Method = "GET"
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
c := client.NewClient("chrome-145", client.WithTimeout(60*time.Second))
defer c.Close()
if req.Proxy != "" {
c.SetProxy(req.Proxy)
}
headers := make(map[string][]string, len(req.Headers))
var userAgent string
for k, v := range req.Headers {
lower := strings.ToLower(k)
if lower == "user-agent" {
userAgent = v
} else {
headers[k] = []string{v}
}
}
var bodyReader io.Reader
if req.Body != "" {
bodyReader = strings.NewReader(req.Body)
}
hcReq := &client.Request{
Method: strings.ToUpper(req.Method),
URL: req.URL,
Headers: headers,
Body: bodyReader,
UserAgent: userAgent,
FetchMode: client.FetchModeNavigate,
}
resp, err := c.Do(ctx, hcReq)
if err != nil {
writeError(w, http.StatusBadGateway, "fetch failed: "+err.Error())
return
}
body, err := resp.Text()
if err != nil {
writeError(w, http.StatusInternalServerError, "read body failed: "+err.Error())
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(FetchResponse{
Status: resp.StatusCode,
Body: body,
Headers: resp.Headers,
})
}
func main() {
const port = "7878"
mux := http.NewServeMux()
mux.HandleFunc("/fetch", fetchHandler)
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"status":"ok"}`)
})
log.Printf("TLS server (httpcloak chrome-145) listening on :%s", port)
log.Fatal(http.ListenAndServe(":"+port, mux))
}
Initialize and build
bash
go mod init tls-server
go get github.com/sardanioss/httpcloak/client
go build -o main main.go
Run the server
bash
./main
The server runs in the foreground. To keep it running in the background, use any process manager (systemd, supervisor, Docker, etc.) or run it in a screen/tmux session.
Verify it's running (in a new terminal)
bash
curl http://localhost:7878/health
Expected: {"status":"ok"}
Note: The TLS server must run on the same machine as your n8n instance. The n8n workflow calls it at
http://localhost:7878/fetch.
Step 2 โ Allow n8n to Call Localhost
By default, n8n blocks HTTP Request nodes from calling localhost addresses (SSRF protection). You need to disable this so your workflows can reach the TLS server on localhost:7878.
Add the N8N_BLOCK_ACCESS_TO_LOCALHOST=false environment variable and restart your n8n instance. How you do this depends on how you run n8n:
If you run n8n directly:
bash
export N8N_BLOCK_ACCESS_TO_LOCALHOST=false
n8n start
If you use Docker:
Add -e N8N_BLOCK_ACCESS_TO_LOCALHOST=false to your docker run command, or add it to the environment section in your docker-compose.yml.
Step 3 โ Using the TLS Server from n8n
The TLS server exposes a single endpoint that accepts any HTTP request and forwards it with a Chrome TLS fingerprint.
API Reference
Endpoint: POST http://localhost:7878/fetch
Request body (JSON):
json
{
"url": "https://example.com",
"method": "GET",
"headers": {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
"cookie": "cf_clearance=abc123; session=xyz"
},
"proxy": "http://user:pass@host:port",
"body": ""
}
| Field | Type | Required | Description |
|---|---|---|---|
url |
string | Yes | The target URL to fetch |
method |
string | No | HTTP method โ defaults to GET |
headers |
object | No | Key-value pairs of HTTP headers to send |
proxy |
string | No | Proxy URL in http://user:pass@host:port format |
body |
string | No | Request body (for POST/PUT requests) |
Response (JSON):
json
{
"status": 200,
"body": "<html>...</html>",
"headers": { "content-type": ["text/html"], "..." : ["..."] }
}
Configuring the n8n HTTP Request Node
To call the TLS server from an n8n workflow, use an HTTP Request node with these settings:
| Parameter | Value | Description |
|---|---|---|
| Method | POST |
Always POST to the TLS server |
| URL | http://localhost:7878/fetch |
Local TLS server endpoint |
| Content Type | Raw |
Do NOT use JSON โ n8n's JSON mode serializes incorrectly |
| Raw Content Type | application/json |
Tell the TLS server the body is JSON |
| Body | ={{ JSON.stringify({ url: "...", method: "GET", headers: {...}, proxy: "..." }) }} |
The actual request to forward |
Important: Using
contentType: "json"withJSON.stringify()in the body causes n8n to double-serialize, sending{"": ""}instead of your data. Always usecontentType: "raw"withrawContentType: "application/json".
Example: Fetching a Protected Page
In the HTTP Request node body expression:
javascript
={{ JSON.stringify({
url: "https://protected-site.com/data",
method: "GET",
headers: {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"accept-language": "en-US,en;q=0.9"
},
proxy: "http://user:pass@proxy-host:8080"
}) }}
The TLS server will forward this request with a Chrome-145 TLS fingerprint, and the target will see a genuine Chrome browser connection.
Test It
Test the TLS server directly from the command line:
bash
curl -X POST http://localhost:7878/fetch \
-H "Content-Type: application/json" \
-d '{
"url": "https://tls-check.example.com",
"method": "GET",
"headers": {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"
}
}'
You can verify your TLS fingerprint by pointing the server at a JA3/JA4 fingerprint checker โ the result should match a real Chrome browser, not a Go standard library client.
Import This Workflow
This workflow creates a webhook endpoint that forwards any request through the TLS server with a Chrome TLS fingerprint. Send a POST with url, method, headers, and optional proxy โ the workflow passes it to localhost:7878/fetch and returns the result.
Webhook (POST /tls-fetch) โ Fetch via TLS Server โ Respond to Webhook
Copy the JSON below and import it into n8n via Menu โ Import from JSON.
Click to expand workflow JSON
json
{
"name": "TLS Fetch โ Chrome Fingerprint Proxy",
"nodes": [
{
"parameters": {
"content": "## TLS Fetch โ Chrome Fingerprint Proxy\n\n**Who it's for:** Developers needing HTTP requests with authentic browser TLS fingerprints.\n\n**What it does:** Proxies HTTP requests through a Go TLS server (httpcloak) that mimics Chrome's TLS fingerprint, bypassing bot detection.\n\n**How it works:**\n1. Webhook receives the target URL and request details\n2. Request is forwarded to the local TLS server with Chrome fingerprint\n3. Response is returned to the caller\n\n**Setup:**\n1. Ensure the TLS server (httpcloak) is running on port 7878\n2. Activate the workflow\n3. POST to the webhook URL with your request details",
"height": 494,
"width": 460,
"color": 1
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-720,
-300
],
"id": "sticky-blog-main-1773678228122-1",
"name": "Sticky Note"
},
{
"parameters": {
"httpMethod": "POST",
"path": "tls-fetch",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-200,
0
],
"id": "tls00001-0001-0001-0001-000000000001",
"name": "Receive Solver Request",
"webhookId": "tls00001-aaaa-bbbb-cccc-000000000001"
},
{
"parameters": {
"method": "POST",
"url": "http://localhost:7878/fetch",
"sendBody": true,
"contentType": "raw",
"rawContentType": "application/json",
"body": "={{ JSON.stringify($json.body) }}",
"options": {
"timeout": 60000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
100,
0
],
"id": "tls00001-0001-0001-0001-000000000002",
"name": "Fetch via TLS Server"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify($json) }}",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
400,
0
],
"id": "tls00001-0001-0001-0001-000000000003",
"name": "Respond to Webhook"
}
],
"connections": {
"Receive Solver Request": {
"main": [
[
{
"node": "Fetch via TLS Server",
"type": "main",
"index": 0
}
]
]
},
"Fetch via TLS Server": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
Conclusion
You've set up a TLS fingerprint spoofing server that makes n8n's HTTP requests look like genuine Chrome browser traffic at the network level. This is essential for scraping websites protected by anti-bot services that inspect TLS fingerprints.
This TLS server is useful for bypassing:
- Cloudflare Bot Management โ full-page challenges that check TLS fingerprints
- Akamai Bot Manager โ enterprise bot detection using TLS analysis
- DataDome โ behavioral + TLS fingerprint analysis
- PerimeterX / HUMAN โ device + TLS fingerprinting
- Many other anti-bot services that CapSolver supports
The httpcloak library with its Chrome-145 preset handles JA3/JA4 fingerprint spoofing, HTTP/2 SETTINGS frames, ALPN negotiation, and header ordering โ making your requests indistinguishable from a real Chrome browser at the TLS layer.
Need to solve CAPTCHAs alongside TLS spoofing? Check out CapSolver โ it integrates directly with n8n as an official node and supports Cloudflare Challenge, Turnstile, reCAPTCHA, and many more. Use bonus code n8n for an extra 8% bonus on your first recharge!

Frequently Asked Questions
What is TLS fingerprinting?
TLS fingerprinting is a technique where servers analyze the characteristics of your TLS ClientHello message โ including cipher suites, extensions, and their ordering โ to identify what software is making the connection. Each HTTP client (Chrome, Firefox, curl, Go, Python) has a unique fingerprint pattern.
Why can't I just set the User-Agent header to Chrome?
The User-Agent header is an HTTP-level attribute. TLS fingerprinting happens at a lower level โ during the TLS handshake, before any HTTP headers are sent. Anti-bot services compare both layers: if your User-Agent says Chrome but your TLS fingerprint says Go/Python, the request is flagged as a bot.
What is httpcloak?
httpcloak is a Go library that spoofs real browser TLS profiles. It handles JA3/JA4 fingerprint matching, HTTP/2 SETTINGS frames, ALPN negotiation, and header ordering. The chrome-145 preset makes connections indistinguishable from a real Chrome 145 browser.
Can I use a different Chrome version preset?
Yes. httpcloak supports multiple browser presets. Check the httpcloak documentation for available presets. To change the preset, modify client.NewClient("chrome-145", ...) in main.go to your desired browser profile.
Does this work with n8n Cloud?
Not easily. The TLS server is a local Go binary that must run on the same machine as n8n so workflows can call http://localhost:7878/fetch. n8n Cloud does not allow running local services alongside workflows. You need a self-hosted n8n instance.
Can I run the TLS server on a different machine?
Yes, but you'll need to update the URL in your n8n HTTP Request nodes from http://localhost:7878/fetch to http://your-server-ip:7878/fetch, and ensure port 7878 is accessible. You'll also need to disable n8n's SSRF protection or whitelist the server's IP.
How do I update the Chrome preset when a new version is released?
Update the httpcloak dependency: go get -u github.com/sardanioss/httpcloak/client, change the preset string in main.go to the new version, rebuild with go build -o main main.go, and restart the server.
Does the TLS server support concurrent requests?
Yes. Go's HTTP server handles concurrent requests natively. Each request creates a new httpcloak client instance with its own TLS connection. For high-volume workloads, monitor memory usage since each connection maintains its own TLS state.
What's the performance overhead?
The TLS server adds minimal latency โ typically 10-50ms for the local proxy hop. The majority of request time is spent on the actual HTTPS connection to the target. The Chrome TLS handshake is slightly heavier than Go's default, but this is negligible in practice.
How do I keep the TLS server running after server reboots?
Use any process manager โ systemd, supervisor, Docker, or similar โ to register the TLS server as a service that starts on boot. For a quick setup, you can also run it inside a screen or tmux session.
Compliance Disclaimer: The information provided on this blog is for informational purposes only. CapSolver is committed to compliance with all applicable laws and regulations. The use of the CapSolver network for illegal, fraudulent, or abusive activities is strictly prohibited and will be investigated. Our captcha-solving solutions enhance user experience while ensuring 100% compliance in helping solve captcha difficulties during public data crawling. We encourage responsible use of our services. For more information, please visit our Terms of Service and Privacy Policy.
More

How to Use CapSolver in n8n: The Complete Guide to Solving CAPTCHA in Your Workflows
Learn how to integrate CapSolver with n8n to solve CAPTCHAs and build reliable automation workflows with ease.

Lucas Mitchell
18-Mar-2026

How to Solve Cloudflare Turnstile Using CapSolver and n8n
Build a Cloudflare Turnstile solver API using CapSolver and n8n. Learn how to automate token solving, submit it to websites, and extract protected data with no coding.

Ethan Collins
18-Mar-2026

How to Solve reCAPTCHA v2/v3 Using CapSolver and n8n
Build a eCAPTCHA v2/v3 solver API using CapSolver and n8n. Learn how to automate token solving, submit it to websites, and extract protected data with no coding.

Lucas Mitchell
18-Mar-2026

How to Solve TLS Fingerprinting in n8n with CapSolver
Solve TLS fingerprinting in n8n with CapSolver. Make requests appear as real browsers and avoid bot detection blocks.

Ethan Collins
18-Mar-2026

How to Solve Visual Puzzles in n8n with CapSolver
Solve visual CAPTCHAs with CapSolver Vision Engine in n8n. Handle sliders, rotation, object selection, and GIF OCR instantly.

Ethan Collins
18-Mar-2026

How to Solve ImageToText Using CapSolver and n8n
Automate captcha solving with n8n using CapSolver Image to Text. Instantly convert images to text and streamline workflows.

Ethan Collins
18-Mar-2026

