CAPSOLVER
Blog
How to Solve TLS Fingerprinting in n8n with CapSolver

How to Solve TLS Fingerprinting in n8n with CapSolver

Logo of 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:

  1. Your n8n workflow sends an HTTP request to a protected website
  2. The TLS handshake begins โ€” your client sends its ClientHello
  3. The anti-bot service records the JA3/JA4 fingerprint from the handshake
  4. The fingerprint matches Go/Python/Node.js โ€” not Chrome or Firefox
  5. 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 Copy
mkdir -p ~/tls-server && cd ~/tls-server

Create a file called main.go with the following content:

go Copy
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 Copy
go mod init tls-server
go get github.com/sardanioss/httpcloak/client
go build -o main main.go

Run the server

bash Copy
./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 Copy
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 Copy
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 Copy
{
  "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 Copy
{
  "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" with JSON.stringify() in the body causes n8n to double-serialize, sending {"": ""} instead of your data. Always use contentType: "raw" with rawContentType: "application/json".

Example: Fetching a Protected Page

In the HTTP Request node body expression:

javascript Copy
={{ 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 Copy
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.

Copy
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 Copy
{
  "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!

CapSolver bonus code banner

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