Automating Browser Profiles in Go With chromedp and the REST Endpoint
I spent a weekend last month porting a Python scraper to Go. Classic hubris — "this'll be easy." The scraper worked fine until I pointed it at a site behind Cloudflare. Blocked within 3 requests. Embarrassing. The Go ecosystem has chromedp, which is solid for CDP automation, but it launches stock Chromium. Stock Chromium screams "I'm automated" through about 40 fingerprint signals.
The fix wasn't complicated once I figured it out: launch an antidetect profile via REST, grab the CDP WebSocket endpoint, connect chromedp to that endpoint instead of spawning a fresh browser. Real fingerprint protection at the engine level. No stealth plugins. No JavaScript monkey-patches.
Here's the complete workflow for Go developers who want proper detection evasion without abandoning chromedp.
What We're Building
By the end of this, you'll have a Go program that:
- Launches a JustBrowser profile via the REST API
- Connects chromedp to the profile's CDP endpoint
- Navigates to a target site with full fingerprint protection
- Extracts data and closes the session cleanly
The profile handles canvas, WebGL, audio, fonts, Client Hints, and 40+ identity parameters at the native C++ level. Your Go code just drives the browser — the hard detection evasion work is already done.
Prerequisites
- Go 1.21+ (earlier versions work, but 1.21's improved context handling is nice)
- JustBrowser ($9.99/mo, one plan, unlimited profiles and API access) — the 7-day trial is full access, REST API included, so you can build this before you pay
- chromedp package:
go get -u github.com/chromedp/chromedp - A configured profile in JustBrowser with proxy assigned (residential recommended)
- Basic familiarity with Go's context package
Step 1: Understand the API Surface
JustBrowser's REST API runs locally on 127.0.0.1 on the port you configure in the app (default 36542), under the base path /api/v1, and every call needs an Authorization: Bearer token from the app's API/AI tab. The two endpoints you'll use most:
POST /api/v1/profiles/{profileId}/start
→ Returns: { "data": { "profile_id": "...", "status": "running", "cdp_url": "ws://127.0.0.1:PORT/devtools/browser/UUID", "launched_at": "..." } }
POST /api/v1/profiles/{profileId}/stop
→ Closes the browser instance
The cdp_url is a standard Chrome DevTools Protocol WebSocket. chromedp speaks CDP natively, so no translation layer required.
(If you've used Playwright's connectOverCDP or Puppeteer's connect({ browserWSEndpoint }), this is the exact same thing — just from Go instead of Node.)
Step 2: Launch a Profile from Go
First, a helper function to start a profile and return the WebSocket URL:
package main
import (
"encoding/json"
"fmt"
"net/http"
"strings"
)
type LaunchResponse struct {
Data struct {
ProfileID string `json:"profile_id"`
Status string `json:"status"`
CdpURL string `json:"cdp_url"`
} `json:"data"`
}
func launchProfile(apiBase, apiToken, profileID string) (string, error) {
url := fmt.Sprintf("%s/api/v1/profiles/%s/start", apiBase, profileID)
req, _ := http.NewRequest("POST", url, strings.NewReader(`{"headless": false}`))
req.Header.Set("Authorization", "Bearer "+apiToken)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", fmt.Errorf("failed to launch profile: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", fmt.Errorf("launch failed with status %d", resp.StatusCode)
}
var result LaunchResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", fmt.Errorf("failed to parse response: %w", err)
}
return result.Data.CdpURL, nil
}
The headless: false matters. Headed mode passes more detection checks — headless Chrome has distinct WebGL renderer strings and missing plugin signatures. Plan the deployment around that: JustBrowser runs on Windows x64 and macOS (Apple Silicon), so your Go program and the browser both want a host with a real desktop session, not a stripped-down VM.
Step 3: Connect chromedp to the Remote Browser
Here's where chromedp diverges from its usual pattern. Normally you'd use chromedp.NewExecAllocator to spawn Chrome. Instead, use chromedp.NewRemoteAllocator with the WebSocket URL:
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/chromedp/chromedp"
)
func main() {
apiBase := "http://127.0.0.1:36542"
apiToken := os.Getenv("JUSTBROWSER_API_TOKEN") // from the app's API/AI tab
profileID := "your-profile-id-here"
// Launch the antidetect profile
cdpURL, err := launchProfile(apiBase, apiToken, profileID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Connected to: %s\n", cdpURL)
// Create remote allocator pointing to the profile's CDP endpoint
allocCtx, allocCancel := chromedp.NewRemoteAllocator(
context.Background(),
cdpURL,
)
defer allocCancel()
// Create browser context
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
// Set a timeout
ctx, cancel = context.WithTimeout(ctx, 60*time.Second)
defer cancel()
// Run your automation
var title string
err = chromedp.Run(ctx,
chromedp.Navigate("https://abrahamjuliot.github.io/creepjs/"),
chromedp.Sleep(3*time.Second), // Let CreepJS compute fingerprints
chromedp.Title(&title),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Page title: %s\n", title)
}
That's it. chromedp now controls a browser with native fingerprint protection. The profile's canvas hash, WebGL renderer, audio fingerprint — all 40+ identity parameters — are already spoofed at the C++ level before any JavaScript executes.
Step 4: Extract Data Like Normal
chromedp works identically once connected — selectors, waits, screenshots, everything:
func scrapeWithProtection(ctx context.Context, targetURL string) (string, error) {
var content string
err := chromedp.Run(ctx,
chromedp.Navigate(targetURL),
chromedp.WaitVisible(`body`, chromedp.ByQuery),
chromedp.Sleep(2*time.Second), // Realistic page load time
chromedp.OuterHTML(`body`, &content, chromedp.ByQuery),
)
return content, err
}
Add realistic delays. Seriously. I see people skip this constantly and wonder why they're still getting blocked. Bots that navigate instantly and extract immediately get flagged on behavioral signals even with perfect fingerprints. A 2-3 second delay on page load isn't paranoia — it's how humans actually browse. (I once spent two days debugging "fingerprint issues" that turned out to be 50ms page transitions. Not my proudest moment.)
Step 5: Clean Up Properly
Always stop profiles when you're done. Orphaned browser processes eat RAM:
func stopProfile(apiBase, apiToken, profileID string) error {
url := fmt.Sprintf("%s/api/v1/profiles/%s/stop", apiBase, profileID)
req, _ := http.NewRequest("POST", url, nil)
req.Header.Set("Authorization", "Bearer "+apiToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// In main(), after automation completes:
defer stopProfile(apiBase, apiToken, profileID)
Step 6: Run Multiple Profiles Concurrently
Go's concurrency model makes parallel profile management straightforward. Here's a pattern for running N profiles simultaneously:
func runParallel(apiBase, apiToken string, profileIDs []string, work func(context.Context) error) error {
var wg sync.WaitGroup
errChan := make(chan error, len(profileIDs))
for _, pid := range profileIDs {
wg.Add(1)
go func(profileID string) {
defer wg.Done()
cdpURL, err := launchProfile(apiBase, apiToken, profileID)
if err != nil {
errChan <- err
return
}
defer stopProfile(apiBase, apiToken, profileID)
allocCtx, allocCancel := chromedp.NewRemoteAllocator(
context.Background(),
cdpURL,
)
defer allocCancel()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
if err := work(ctx); err != nil {
errChan <- err
}
}(pid)
}
wg.Wait()
close(errChan)
// Return first error if any
for err := range errChan {
if err != nil {
return err
}
}
return nil
}
Each profile is a separate browser process with its own fingerprint configuration. Run 10 profiles and you have 10 distinct "users" — different canvas hashes, WebGL renderers, timezone/language combinations, the works.
(I learned the hard way that you need to manage RAM carefully here. Each headed profile runs around 300-500MB. A 16GB machine tops out around 25-30 concurrent profiles before things get unstable.)
Common Errors and How to Fix Them
Connection refused on WebSocket endpoint
websocket: bad handshake
The profile isn't running yet. The launch endpoint returns before the browser is fully ready. Add a brief sleep after launch, or implement a retry loop:
func waitForEndpoint(cdpURL string, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
conn, _, err := websocket.DefaultDialer.Dial(cdpURL, nil)
if err == nil {
conn.Close()
return nil
}
time.Sleep(200 * time.Millisecond)
}
return fmt.Errorf("endpoint not ready after %v", timeout)
}
Context deadline exceeded
Your timeout is too short for the page load + processing. Heavy SPAs can take 10-15 seconds to fully render. Bump the timeout or add explicit waits for specific elements rather than relying on page load events.
Profile already running
You're trying to launch a profile that's already active. Either stop it first or use a different profile ID. The API doesn't support multiple connections to the same profile (by design — it would create fingerprint inconsistencies).
Empty response body
The selector you're targeting doesn't exist or isn't visible. Use chromedp.WaitVisible or chromedp.WaitReady before extraction. SPAs render content asynchronously — you'd think this would be obvious but I still mess it up occasionally.
Next Steps
You've got the basic workflow — launch via REST, connect chromedp, automate, clean up. From here:
Proxy rotation: Each profile can have a different proxy assigned. For large-scale scraping, create profile pools by geographic region and rotate through them. We covered proxy matching in the ISP proxy setup tutorial.
Profile warming: Fresh profiles with zero browsing history look suspicious. Like, obviously suspicious. JustBrowser's cookie warm-up engine hits 127 curated sites across 6 categories to build realistic history before you run against your targets. Skip this and you're basically announcing you just created this identity five minutes ago. I'm genuinely baffled by how many tutorials ignore this.
Detection testing: Before pointing your scraper at production targets, verify your setup against CreepJS and FingerprintJS Pro. Both should show unique, consistent fingerprints — not randomized garbage that screams automation. (Hot take: if you're not testing against CreepJS before production, you're basically flying blind. I don't care what your stealth plugin's README claims.) Full walkthrough here.
Cross-language teams: If your team has existing Python/Node automation, the same profiles work everywhere. Our Playwright/Puppeteer integration guide covers the Node side, and the Python quickstart handles requests-based workflows. For email deliverability in your outreach automation, check JustEmails.
The Go + chromedp + antidetect browser stack gives you Go's performance benefits — low memory, fast execution, actual concurrency that doesn't make you want to throw your laptop — plus fingerprint protection that works against modern detection. Six weeks on three scraping projects. Zero blocks on sites that flagged my Python/Playwright setup within hours. Your results will vary, obviously, but the difference was night and day for me.
Frequently Asked Questions
Why use chromedp with an antidetect browser instead of regular Chrome?
Regular Chrome exposes detectable fingerprints through WebGL renderer strings, canvas hashes, and CDP artifacts. Antidetect browsers modify the Chromium engine at the C++ level, so the browser genuinely reports spoofed values. chromedp connects via CDP the same way it would to regular Chrome, but the underlying browser has real fingerprint protection that JavaScript-based stealth plugins can't match.
How do I connect chromedp to a JustBrowser profile?
Launch the profile via the REST API endpoint (POST /api/v1/profiles//start with a Bearer token), which returns the CDP WebSocket URL as data.cdp_url. Pass that URL to chromedp.NewRemoteAllocator() instead of using chromedp.NewExecAllocator(). From there, chromedp works exactly as it normally would — the profile handles fingerprint spoofing at the engine level.
Can I run multiple antidetect profiles concurrently in Go?
Yes. Each profile launches as a separate browser process with its own CDP endpoint. Use goroutines to manage multiple chromedp contexts, each connected to a different profile's WebSocket URL. The main constraint is RAM — expect roughly 300-500MB per headed profile on a desktop fingerprint configuration.
Do I need to run headed mode or can I use headless?
Headed mode is strongly recommended for detection evasion. Headless Chrome has distinct WebGL renderer strings, missing plugins, and timing characteristics that detection services flag. Run headed on a machine with a real desktop session — JustBrowser ships for Windows x64 and macOS on Apple Silicon, so the deployment target is a persistent Windows box or a Mac rather than a headless Linux VM.
Try JustBrowser
Native Chromium antidetect browser — not extension-based. Real C++ engine patches at the canvas / WebGL / audio / font / screen layer, so 40+ identity parameters are genuine, not faked. REST API for Playwright, Puppeteer, Selenium. $9.99/month or $99.99/year. 7-day free trial, card required — cancel any time in the seven days and you are not charged. Unlimited profiles.
Get started → · How it differs from Multilogin / GoLogin / AdsPower
Related Posts
Ready to manage multiple accounts?
Seven days free, then $9.99/month — one plan, everything included.