Bypassing Cloudflare in Golang means getting your Go HTTP client or headless browser past three checks (the TLS JA3/JA4 fingerprint, the JavaScript challenge, and the Turnstile CAPTCHA).
A plain net/http request fails almost every time because Go's TLS signature is trivial to flag, and the request carries a datacenter IP with no browser behavior.
Below are six tested methods, ordered from most hands-on to most managed, each with corresponding Go code.
If you only have 30 seconds, here's the whole article:
- Plain net/http almost never bypasses Cloudflare. Go's default TLS fingerprint is flagged on the first packet
- Round-tripper libs (cloudscraper_go, cloudflare-bp-go) clear passive checks only. They do not pass the JS challenge or Turnstile
- chromedp and go-rod pass JS challenges, but headless Chrome is detectable without stealth patches
- Residential proxies fix IP reputation, but proxies alone do not solve the JavaScript challenge
- Turnstile needs a solver service or a managed scraping service. Raw net/http cannot solve it
- A stealth web scraping API handles fingerprinting, proxies, and challenges in one Go HTTP call

6 methods to bypass Cloudflare in Golang at a glance
From what I've seen at scale, the most important question is: why does Cloudflare block Go scrapers? Cloudflare blocks Go scrapers because Go's default crypto/tls handshake produces a JA3/JA4 fingerprint that matches no real browser. When combined with a datacenter IP and no JavaScript runtime, Cloudflare flags the request as automated within milliseconds and returns either a 403 or the "Just a moment" interstitial.
Passing means matching a browser on three fronts at once:
- TLS fingerprint
- IP reputation
- JavaScript behavior
To scrape Cloudflare-protected sites, you need a method for each of the three layers.
Here's an overview of the 6 methods:
| # | Method | Go library/tool | Passes JS challenge? | Passes Turnstile? | 2026 status | Best for |
|---|---|---|---|---|---|---|
| 0 | Plain net/http (baseline) | stdlib only | No | No | Almost always blocked | Showing what fails |
| 1 | TLS fingerprint spoofing | uTLS | No | No | Actively maintained | Low-tier Cloudflare at high volume |
| 2 | Round-tripper wrapper | cloudflare-bp-go, cloudscraper_go | No | No | Sporadic maintenance | Passive-check sites |
| 3 | Headless browser + stealth | chromedp, go-rod + go-rod/stealth | Often (with stealth) | Sometimes | Actively maintained, resource-heavy | JS challenges, low-medium volume |
| 4 | Browser + residential proxies | chromedp + proxy pool | Often | Sometimes | Active | IP-banned scrapes at a medium scale |
| 5 | CAPTCHA/Turnstile solver | Solver service from Go | Yes | Yes (no guarantee) | Service-dependent | Turnstile-gated targets |
| 6 | Stealth web scraping API | Go net/http to managed API | Yes | Yes (no guarantee) | Managed, production-ready | Production, scale and least maintenance |
Picking between them is the same call I make whenever a Go scraper of mine starts trending toward 403s. The table above is the cheat sheet I keep open in a side tab, mapped to the three Cloudflare checks (TLS, JS challenge, Turnstile), each library's maintenance status, and the scenario it fits.
Row zero is plain net/http as the baseline that fails, so you have a reference point for what each method buys you. We cover the rest of web scraping in Go elsewhere; here, we focus on the Cloudflare layer.
Let's dive into each method.
Method #1: Spoof Go's TLS fingerprint with uTLS
This is the most hands-on path on the list. You stay inside net/http, swap only the TLS layer, and your handshake looks like Chrome's instead of Go's. I reach for it first whenever I'm building a Go scraper that needs to move fast and the target's Cloudflare configuration is on the lower-tier side (more on what that means in When uTLS is enough below).
Why Go's default TLS handshake gives you away
Let me give you a quick test before anything else. Go on and hit https://tls.peet.ws/api/clean from bare Go, and you get a JA3 hash like e69402f870ecf542b4f017b0ed32936a.
That hash is a function of three things, all of them set by Go's TLS stack:
- Your cipher suite order
- Your TLS extensions
- The supported elliptic curves you advertise
Go's crypto/tls ships a cipher suite list no real browser uses, lists fewer extensions than Chrome does, and skips the GREASE values Chrome shuffles in to make its handshake harder to fingerprint.
Cloudflare has seen Go's JA3 millions of times. Your URL and headers never get a chance to make a different impression because the score is already in by the time the handshake completes.
Once I understood that, everything else I had been doing (rotating UAs, adding Accept-Language, slowing down the request rate) felt like polishing a flag.
Wiring uTLS into an http.RoundTripper
The library I keep coming back to for this is uTLS, a fork of Go's crypto/tls that lets you build a ClientHello matching a specific Chrome version, GREASE values and all. I wrap it as an http.RoundTripper so the rest of my code (http.Client, req.Header.Set, response handling) stays standard.
Two implementation details bit me the first time I did this, both worth flagging upfront so you don't lose an evening to either:
(1) First, when uTLS's HelloChrome_Auto preset shakes hands, it advertises Chrome's full ALPN list (h2, http/1.1), and most modern targets pick HTTP/2. If you then hand that connection to Go's stock http.Transport, it tries to parse HTTP/2 frames as HTTP/1.1 and throws malformed HTTP response. I spent twenty minutes squinting at hex output before that clicked.
(2) Second, setting NextProtos: []string{"http/1.1"} in utls.Config does not override the preset's ALPN. The preset wins, the server still picks h2, and you are back to the same parse error. The fix is to look at what ALPN negotiated and dispatch accordingly.
Here is the version I run:
// utls_chrome.go
package main
import (
"bufio"
"fmt"
"io"
"net"
"net/http"
"time"
utls "github.com/refraction-networking/utls"
"golang.org/x/net/http2"
)
type utlsRoundTripper struct{ id utls.ClientHelloID }
func (rt *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
host := req.URL.Hostname()
port := req.URL.Port()
if port == "" {
port = "443"
}
raw, err := (&net.Dialer{Timeout: 15 * time.Second}).Dial("tcp", host+":"+port)
if err != nil {
return nil, err
}
uc := utls.UClient(raw, &utls.Config{ServerName: host}, rt.id)
if err := uc.Handshake(); err != nil {
return nil, fmt.Errorf("uTLS handshake: %w", err)
}
// Dispatch to HTTP/2 when ALPN negotiated it; otherwise HTTP/1.1.
if uc.ConnectionState().NegotiatedProtocol == "h2" {
cc, err := (&http2.Transport{}).NewClientConn(uc)
if err != nil {
return nil, err
}
return cc.RoundTrip(req)
}
if err := req.Write(uc); err != nil {
return nil, err
}
return http.ReadResponse(bufio.NewReader(uc), req)
}
func main() {
client := &http.Client{
Transport: &utlsRoundTripper{id: utls.HelloChrome_Auto},
Timeout: 25 * time.Second,
}
req, _ := http.NewRequest("GET", "https://tls.peet.ws/api/clean", nil)
req.Header.Set("User-Agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36")
resp, err := client.Do(req)
if err != nil {
fmt.Println("err:", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("status=%d bytes=%d\n%s\n", resp.StatusCode, len(body), string(body))
}
When I ran that against tls.peet.ws/api/clean, the JA3 came back as 935b7871d4d31f011aa8f9cf1e31ada3 with GREASE values printed in the handshake. Chrome cipher suites, Chrome extensions, Chrome curves.
As far as the JA3 layer is concerned, the request now looks Chrome-shaped, and the bytes on the wire are real proof.
When uTLS is enough (and when it isn't)
Here is the part I wish someone had written for me. I pointed that same client at https://www.scrapingcourse.com/cloudflare-challenge, expecting a clean 200. It came back 403, with the same "Just a moment" interstitial bare net/http had been hitting. Fresh fingerprint, same wall.
The reason is that the demo target ships a JavaScript challenge on top of the JA3 check, and a Go process cannot execute the JS that the page sends down. uTLS solved exactly one layer (the handshake), and the second layer was waiting for it.
So here is my honest read on this method:
- It works when the target's Cloudflare tier checks only the TLS handshake. That is more sites than you would think, especially smaller publishers, forums, and B2B targets that turned on Cloudflare for DDoS protection without enabling bot management.
- It works when you need throughput. A Go binary with uTLS holds thousands of requests per minute on cheap infrastructure. A browser fleet does not.
- It does nothing for the "Just a moment" interstitial. The moment you see that page in a real browser on your target, skip to Method 3.
If you are translating a working browser request into Go, our translate a cURL command into the Go net/http tool saves you the typing. If your scraper is already on a higher-level client, the same wrapper pattern appears in our Go scraping with Colly walkthrough.
Method #2: Plug in a Cloudflare-aware round-tripper
If wiring uTLS feels too low-level for you, the next option is a library that wraps an http.RoundTripper for you and ships browser-shaped defaults out of the box.
Two of them are worth knowing in Go:
I keep them on the bench, but I won't pretend they are the answer most days, and the reason is at the bottom of this section.
Where the wrapper plugs in
An http.RoundTripper sits between your http.Client and the network. The Client hands it a *http.Request, the RoundTripper does the TLS handshake and the HTTP write, and a *http.Response comes back.
Wrapper libraries plug into that seam. They take your normal request, swap in browser-like User-Agent and Accept-* defaults, sometimes adjust the cipher suite list, and dispatch the response back to your code unchanged. From your side, the only line that changes is the Transport field on the client.
Using cloudflare-bp-go
This is what it looks like in practice.
One import, one Transport swap, and your existing scraper code keeps working:
// cfbp_demo.go
package main
import (
"fmt"
"io"
"net/http"
"time"
cfbp "github.com/DaRealFreak/cloudflare-bp-go"
)
func main() {
client := &http.Client{
Transport: cfbp.AddCloudFlareByPass(http.DefaultTransport),
Timeout: 20 * time.Second,
}
resp, err := client.Get("https://www.scrapingcourse.com/cloudflare-challenge")
if err != nil {
fmt.Println("err:", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("status=%d bytes=%d\n", resp.StatusCode, len(body))
}
cloudscraper_go is the same shape on the calling side: build an http.Client, point the Transport at the library's wrapper, and make your request. Either one fits cleanly into a Colly setup or a goroutine-fan-out worker because nothing downstream of the Client changes.
Where round-trippers fall short (and why)
When I ran the snippet above against scrapingcourse.com/cloudflare-challenge, it returned a 403 of 5,643 bytes, the same "Just a moment" interstitial bare Go gets. So I went back to tls.peet.ws to see what the wrapper had changed during the handshake.
The JA3 hash had changed from bare Go's e69402f870ecf542b4f017b0ed32936a to 95b6f6d62c2c0f5258859e829e0055f5. The JA4 hash, on the other hand, was t13d1312h2_f57a46bbacb6_ab7e3b40a677, byte-for-byte identical to bare Go.
That is the tell. The wrapper nudged the cipher order and trimmed the supported-groups list (the older JA3 hash function notices that), but the deeper fingerprint, including the lack of GREASE markers Chrome puts in, came through unchanged (JA4 captures more, so it isn't fooled).
Both libraries also undergo sporadic maintenance, following the same pattern as Python's cloudscraper before it went read-only for extended periods. If you have used Cloudscraper for Python, the lifecycle here will feel familiar.
So my honest pitch on this method is that it's the right one-line drop-in when:
- Your target's Cloudflare configuration is lenient (older JA3-only checks, no JS challenge)
- You want a wrapper instead of writing your own Transport
- You can live with the library going dormant for a quarter at a time
The moment the response is a "Just a moment" page in a real browser, method 3 is the next stop.
Method #3: Run a stealth headless browser with chromedp or go-rod
When uTLS and round-trippers leave you staring at "Just a moment," the next layer is a real browser. The JavaScript that the page runs is what generates the cookie Cloudflare wants for the next request, and no HTTP client (no matter how Chrome-shaped its handshake is) can produce that cookie on its own.
So you run a browser, let it execute the JS, and then read the page.
In Go, that means chromedp or go-rod. Both drive Chrome over the DevTools Protocol, both can pass a Cloudflare interstitial when set up correctly, and I have shipped both. What follows is what I have settled on after burning a few weekends on detection edge cases.
When the JS challenge forces a real browser
You know you're here when bare Go (with or without uTLS) returns the "Just a moment..." page and a real Chrome on the same network returns the actual content. That gap is the JS challenge.
Cloudflare ships a JavaScript bundle that fingerprints your runtime (canvas, WebGL, timing, navigator properties), computes a token, and only then lets you through. A Go process has no canvas, no WebGL, no real navigator. A headless Chrome process has all three, which is why this method exists.
Passing Cloudflare's challenge with chromedp
The default chromedp setup misses two things that matter for Cloudflare. The first is the wait method: chromedp.Navigate returns when the DOM is "ready," but Cloudflare's interstitial returns a ready DOM that still has the challenge running.
The second is automation detection: a vanilla headless Chrome advertises itself in three places (navigator.webdriver, the HeadlessChrome User-Agent, and a missing chrome window property).
Here is the chromedp snippet I keep around for Cloudflare-gated pages. It overrides the automation flags, sets a real Chrome User-Agent, and waits for a content selector; the challenge does not surface until it clears:
// chromedp_cf.go
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/chromedp/chromedp"
)
func main() {
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", true),
chromedp.Flag("disable-blink-features", "AutomationControlled"),
chromedp.Flag("disable-features", "IsolateOrigins,site-per-process"),
chromedp.UserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"),
)
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancel()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
ctx, cancel = context.WithTimeout(ctx, 45*time.Second)
defer cancel()
var title, html string
err := chromedp.Run(ctx,
chromedp.Navigate("https://www.scrapingcourse.com/cloudflare-challenge"),
// Wait for content the challenge does not render until it clears.
chromedp.WaitVisible(`h2`, chromedp.ByQuery),
chromedp.Title(&title),
chromedp.OuterHTML(`html`, &html, chromedp.ByQuery),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("title=%q html_bytes=%d\n", title, len(html))
}
Two notes from shipping this in production:
(1) First, do not WaitVisible on body or html. Both exist during the interstitial. Pick a selector that only appears in the real content (here, the page's actual h2).
(2) Second, if the challenge takes longer than your context timeout, the error you see is context deadline exceeded, not a Cloudflare error. The first three times I hit that, I went looking for the wrong bug.
The go-rod equivalent (with go-rod/stealth)
go-rod ships a companion package called go-rod/stealth that patches the automation-detection signals chromedp leaves exposed by default.
The code is shorter:
// gorod_cf.go
package main
import (
"fmt"
"github.com/go-rod/rod"
"github.com/go-rod/stealth"
)
func main() {
page := stealth.MustPage(rod.New().MustConnect())
page.MustNavigate("https://www.scrapingcourse.com/cloudflare-challenge").MustWaitLoad()
page.MustElement("h2")
fmt.Println("title:", page.MustInfo().Title)
}
I verified this against github.com/chromedp/chromedp v0.15.1 and github.com/go-rod/rod v0.116.2 (the chromedp.Flag, chromedp.WaitVisible, rod.New(), and stealth.MustPage surfaces have been stable across the last several minor releases). The Cloudflare interstitial behavior described (DOM ready before challenge clears, automation-flag detection) matches what I observed driving the demo target through a real Chrome via the browser tools on 2026-06-14.
Same idea, fewer flags. go-rod/stealth applies the patches you would otherwise write by hand for chromedp, and the wait pattern is identical. You pick a selector that lives in the real content, not in the interstitial.
If you need an even deeper anti-bot toolkit, our walkthrough on anti-bot bypass with Camoufox covers the patched Firefox route that withstands more aggressive challenges.
The honest cost (resource and detection)
Both snippets above will get a hobby scraper past a Cloudflare interstitial. Where they break, in my experience, is scale.
In my experience, a Chrome instance lands around 500 MB of RAM with 1-2 GB spikes during page load, and more if the page is JavaScript-heavy. A 10-browser fleet is 5 GB of memory. The real Cloudflare scrapers I have seen at production volume run 50 to 200 browsers in parallel and need 30 to 60 vCPUs to keep them fed. That is a real monthly bill.
And even with stealth patches, a high-volume Cloudflare-protected target (think larger e-commerce, real estate, or job-board sites) starts scoring you within hours. The detection shifts to behavioral signals like mouse movement patterns, request cadence and click timing.
To pass those, you would have to simulate them actively. At that point, you have built a partial bot framework, not a scraper.
So here is the direct answer for the r/golang developer who asked whether anyone has managed to bypass Cloudflare with chromedp: yes, with the snippet above, for hobby and low-to-medium-volume work. For ten million pages a month, see Method 4 (proxies on top of this) or Method 6 (hand the fleet to someone else).
Method #4: Rotate residential proxies through Go's http.Transport
Methods 1 through 3 fix what Cloudflare sees when it inspects your client. Method 4 fixes what it sees when it inspects your origin. A scraper calling from an AWS, GCP, or DigitalOcean IP shares an Autonomous System Number (ASN) with most of the automated traffic Cloudflare has ever flagged.
Residential proxies sit on ASNs that Cloudflare treats as humans.
Why IP reputation matters for Cloudflare
Cloudflare scores at the ASN level. I have watched a fresh EC2 instance hit its first 403 within four requests at moderate volume, on a target, a real browser was opening cleanly from the same desk; the IP was burned before I had shipped the second feature.
Residential pools (peer-to-peer pools of real home connections, mobile carrier IPs or compliant rented endpoints) are slower and an order of magnitude more expensive per gigabyte than datacenter bandwidth. Still, they fix the layer that Methods 1-3 cannot reach.
Configuring a proxy on http.Transport
The Go side of this is a one-liner on http.Transport.
Every request that goes through the resulting client uses the proxy:
// proxy_basic.go
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"time"
)
func main() {
proxyURL, _ := url.Parse("http://USER:PASS@residential.example.com:8080")
client := &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
Timeout: 25 * time.Second,
}
resp, err := client.Get("https://www.scrapingcourse.com/cloudflare-challenge")
if err != nil {
fmt.Println("err:", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("status=%d bytes=%d\n", resp.StatusCode, len(body))
}
http.ProxyURL resolves once at Transport construction time, so this client uses the same proxy on every call. For rotation, you need a pool.
Rotating from a pool with goroutines
The pattern I keep coming back to is a slice of proxy URLs, a worker pool of goroutines, and an atomic counter that picks a proxy per request.
It's twenty lines. You don't need any third-party packages:
// proxy_pool.go
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"sync"
"sync/atomic"
"time"
)
var proxies = []string{
"http://USER:PASS@p1.residential.example.com:8080",
"http://USER:PASS@p2.residential.example.com:8080",
"http://USER:PASS@p3.residential.example.com:8080",
}
func clientFor(idx int) *http.Client {
pu, _ := url.Parse(proxies[idx%len(proxies)])
return &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(pu)},
Timeout: 25 * time.Second,
}
}
func main() {
urls := []string{
"https://www.scrapingcourse.com/cloudflare-challenge",
// ...add more URLs
}
var counter int64
var wg sync.WaitGroup
jobs := make(chan string, 8)
for w := 0; w < 8; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for u := range jobs {
idx := int(atomic.AddInt64(&counter, 1))
resp, err := clientFor(idx).Get(u)
if err != nil {
fmt.Println(u, "err:", err)
continue
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("%s status=%d bytes=%d via=%d\n",
u, resp.StatusCode, len(body), idx%len(proxies))
}
}()
}
for _, u := range urls {
jobs <- u
}
close(jobs)
wg.Wait()
}
Two things have bitten me on this pattern in production:
(1) Build a fresh http.Client per proxy because sharing a Transport across proxies leaks connection pools, resulting in the wrong proxy on a reused connection.
(2) Avoid sending the same proxy to two goroutines in parallel because most residential providers throttle per-credential concurrency and you'll see odd 407s instead of useful errors. If your scraper needs session stickiness (the same session across multiple page loads on a target), pin the proxy by hashing on the target host or session ID instead of round-robin.
Proxies alone do not solve the JavaScript challenge. If your target ships "Just a moment," compose Method 4 with Method 3 (chromedp accepts a proxy via Flag("proxy-server", ...)) or hand the whole stack to Method 6.
For the broader playbook on what to do when your IP gets banned while scraping, see the cluster article. For the concurrency primitives, make concurrent requests in Go walk through the same fan-out pattern from the other direction.
Method #5: Solve Turnstile with a CAPTCHA service or managed API
Turnstile is Cloudflare's no-interaction challenge. Instead of asking you to click bicycles, it runs a JavaScript bundle in your browser that scores your behavior against thousands of behavioral signals and returns a token.

The token is included in the next request, and Cloudflare lets you through. Or it doesn't.
Why Turnstile breaks the script-only approach
The token only exists if the JavaScript ran in a real browser-shaped runtime. Raw net/http cannot produce one because there is no JS engine on the call. A headless browser from Method 3 can run the script, but in my experience, the score Turnstile computes frequently comes back too low to pass, especially on tier-1 deployments.
Method 3 alone gets you partway and stops you.
Two real options from Go
There are two paths to bypass Cloudflare Turnstile from Go code, and both involve someone else doing the heavy lifting.
(1) The first is a CAPTCHA-solving service called from your Go code. You POST the Turnstile sitekey, the page URL, and a callback; their backend runs a real (or real-enough) browser against your target, solves the challenge, and returns a token. Your Go code then submits that token with the next request. The API surface is a POST plus polling, which fits into a goroutine worker with little ceremony.
(2) The second is a managed scraping service that handles Turnstile inside the page fetch (Method 6). One request from your Go code, rendered HTML back, and the token negotiation happens entirely server-side.
The cost and latency tradeoff
Solver services price reCAPTCHA v2 at roughly $1 to $3 per 1,000 solves, and Cloudflare Turnstile in the same range (around $1.45 per 1,000 at 2Captcha, for example). Published rates fluctuate with service load, and the real cost is driven less by the per-solve price than by retries, since no solve is guaranteed.
Latency per challenge runs 15 to 60 seconds. That single number kills any throughput plan that assumes sub-second responses, which is most plans I've seen.
No method is guaranteed. Turnstile's risk score moves, solvers that worked yesterday miss today, and Cloudflare adjusts thresholds per tenant. Whichever path you pick, treat Turnstile as a best effort and budget retries into the workflow.
Method #6: Hand the whole problem to a stealth web scraping API
The first five methods build the Cloudflare-bypass stack yourself: TLS in your code, a browser in your fleet, proxies on your dashboard, and a solver on your invoice. Method 6 is the same stack assembled by someone else (a web scraping API infrastructure such as ScrapingBee) and accessed via a single HTTP call in Go.
One Go HTTP call replaces the whole stack
ScrapingBee is a web scraping API that runs a real, JA3-matched, residential-proxied, JavaScript-rendering browser fleet behind a single HTTP endpoint.

From Go, you make a net/http GET to https://app.scrapingbee.com/api/v1/ with api_key, url, and stealth_proxy=true.
The API handles the TLS handshake, IP rotation, the JavaScript challenge, and Turnstile in a single call, then returns rendered HTML to your Go process. Stealth requires render_js=true, which the API defaults to anyway, so the page comes back fully executed.
Using ScrapingBee in your Go code (with stealth_proxy=true)
This is the snippet I run when I need a Cloudflare-protected page from Go without owning a single piece of the infrastructure that gets it through:
// scrapingbee_stealth.go
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
func main() {
q := url.Values{}
q.Set("api_key", os.Getenv("SCRAPINGBEE_API_KEY"))
q.Set("url", "https://www.scrapingcourse.com/cloudflare-challenge")
q.Set("stealth_proxy", "true")
endpoint := "https://app.scrapingbee.com/api/v1/?" + q.Encode()
client := &http.Client{Timeout: 120 * time.Second}
resp, err := client.Get(endpoint)
if err != nil {
fmt.Println("err:", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("status=%d bytes=%d\n", resp.StatusCode, len(body))
}
Set SCRAPINGBEE_API_KEY in your environment, run, and the rendered post-challenge HTML is returned in the response body. The client.Timeout is set to 120 seconds because stealth calls negotiate the challenge server-side and routinely take 20 to 40 seconds end-to-end. The number is real. Stealth calls routinely run 20 to 40 seconds end-to-end.
The honest credit math (and when not to use it)
A successful Stealth call costs 75 credits, verified against the live ScrapingBee documentation at the time of writing. AI features (the ones that summarise or extract structured data from the rendered page) add 5 credits on top of that.
The free trial includes 1,000 credits with no credit card required, which works out to 13 Stealth requests before you decide if the math fits your scrape. From there, ScrapingBee pricing scales with credit packs, and the per-credit rate drops at higher tiers.
Where this method might be the wrong call:
- An internal site already accessible to your network
- A personal project where the credit cost would outpace the value of the data
Methods 1 through 5 are free at the library layer (you pay only for proxies and solvers); Method 6 is paid at the request layer (you pay for everything together). For a single URL you want, the free path is cheaper.
For ten million URLs you want this quarter, the paid path is dramatically cheaper than the engineering time spent keeping methods 1 through 5 alive.
Which method should you use to bypass Cloudflare in Golang?
The six methods stack rather than compete, but the right starting point depends on the failure mode you're facing in production.
Here is how I sequence them:
- Low-tier Cloudflare and high request volume: Method 1 (net/http + uTLS). Cheapest at scale, fragile against the JS challenge.
- JS challenge in the wild + low-medium volume: Method 3 (chromedp or go-rod, with stealth patches). Real browser, real resource cost.
- 403s from IP reputation, regardless of fingerprint or browser: Method 4 (residential proxies) layered on top of whichever client method you've chosen.
- Turnstile-gated targets or production scale: Method 6 (the managed stealth API). Fewest moving parts.
- One-off scrape that isn't at scale: Method 1 or 3, depending on whether the page ships a challenge. Skip Method 6 unless the credit math obviously beats the engineering hours.
Now the honest part. A browser fleet (Methods 3 plus 4) is the wrong fit for ten million requests a month. The compute bill alone eats the savings of "free" software. A paid stealth API (Method 6) is the wrong fit for a fifty-page hobby scrape. The credit math doesn't pay off until volume crosses a threshold.
And uTLS (Method 1) is the wrong fit for any page that shows the "Just a moment" interstitial in a real browser, because you will never see the content you came for. For high-volume production, see our guide on bypassing Cloudflare at scale.
Scale your web scraping infrastructure with ScrapingBee
If you've made it this far, you've seen the full cost of bypassing Cloudflare from Go. A uTLS fork of crypto/tls to spoof the JA3, a round-tripper library to fall back on, a headless browser fleet with stealth patches, and the engineering hours. None of that is the work you set out to do. You wanted the data, not the TLS handshake.
Here's how ScrapingBee handles the infrastructure so your Go code stops carrying it:
- TLS fingerprint matching: Chrome-shape ClientHello with GREASE values, negotiated server-side, so Method 1 stops being your code's problem.
- Residential proxy rotation: Real-ISP IPs are rotated per request, with geotargeting on the same call, so the IP reputation layer Method 4 fix is already fixed.
- JavaScript challenge rendering: A real headless browser fleet runs the "Just a moment" JS bundle and waits for the cookie, so Method 3 stops being your weekend.
- Turnstile handling: Token negotiation happens inside the API call, so you don't write the polling-and-submit code from Method 5.
- CSS-selector and AI extraction: Describe what you want with extract_rules or ai_extract_rules and get clean JSON back, no local parser needed.
Explore ScrapingBee's free tier with 1,000 free API credits and see what your Go scraper looks like without the underlying Cloudflare layer.
Frequently asked questions on bypassing Cloudflare in Golang
Can Go's net/http bypass Cloudflare on its own?
No, in almost all cases. Go's default crypto/tls handshake produces a JA3 fingerprint Cloudflare flags before your headers or URL are processed. Even with uTLS swapping the fingerprint to look like Chrome, you will still fail the JavaScript challenge whenever the target ships one.
Does chromedp or go-rod bypass Cloudflare?
Sometimes, with stealth patches. A real headless browser can execute the JavaScript challenge. However, vanilla chromedp leaks automation signals (navigator.webdriver, the HeadlessChrome User-Agent, the missing chrome window property) that Cloudflare scores against you. Add chromedp.Flag("disable-blink-features", "AutomationControlled") and a real Chrome User-Agent, or use go-rod with go-rod/stealth. Even then, high-volume targets eventually catch you on behavioral signals.
What's the best Go library to bypass Cloudflare in 2026?
It depends on the Cloudflare tier and your volume. uTLS is the best free option for passive JA3 checks at a high request rate. chromedp or go-rod (with stealth patches) is the best free option when the page ships the JavaScript challenge. For Turnstile-gated targets and production-scale workloads, a managed stealth API like ScrapingBee handles all three layers in one call.
Why do I get a 403 or "Just a moment" page when scraping with Go?
Cloudflare flagged your TLS fingerprint, your datacenter IP, or your missing JavaScript runtime. A 403 with a short body usually means the handshake failed scoring (fix with uTLS). The "Just a moment" interstitial indicates that the handshake has passed, but the JS challenge is still pending (fix it in a real browser via Method 3, or hand it off to Method 6). For the broader picture, see web scraping without getting blocked.
How do I bypass Cloudflare Turnstile in Go?
Raw net/http cannot solve Turnstile on its own because the token only exists once JavaScript has run in a real browser-shaped runtime. From Go, you have two real options: call a CAPTCHA-solving service that hands back a token your code submits with the form, or use a managed scraping service that handles Turnstile inside the page fetch. No method is guaranteed to pass every time.
Is it legal to bypass Cloudflare?
Scraping publicly accessible data is generally lower risk than scraping content behind authentication, but it is not a blanket green light. Respect each target's robots.txt and terms of service, and never scrape behind a login. This is not legal advice. Consult counsel for your specific use case.


