Nossa participação: somos a Geonode e vendemos proxies, e automação de navegador é a coisa mais faminta de banda que você pode fazer por um. Um navegador headless busca cada imagem, fonte, script e preload de vídeo, então rodar chromedp por tráfego medido custa cerca de uma ordem de magnitude a mais do que requisições HTTP cruas para as mesmas páginas. Há uma seção sobre como cortar isso, e a técnica nela vai economizar mais do que escolher um provedor mais barato. A configuração de proxy em si são três linhas e tem uma pegadinha de verdade, que também está coberta.
O que é o chromedp
O projeto se descreve como "a faster, simpler way to drive browsers supporting the Chrome DevTools Protocol in Go without external dependencies".
Essa última cláusula é o argumento de venda. Selenium precisa de um binário de driver casado com a versão do navegador; Playwright envia o próprio runtime. chromedp fala o DevTools Protocol direto por um websocket, então um binário Go mais uma instalação do Chrome é o deploy inteiro.
É licença MIT e é mantido ativamente — a versão 0.15.1 saiu em abril de 2026, com commits até julho, conferidos em setembro de 2026.
A instalação é banal:
go get -u github.com/chromedp/chromedp
Os bindings de protocolo gerados ficam num pacote companheiro, github.com/chromedp/cdproto, ao qual você recorre quando precisa de algo que a API de alto nível não envolve.
O modelo de context
A parte a entender primeiro, porque todo o resto segue dela.
chromedp usa context.Context para dois trabalhos ao mesmo tempo: cancelamento, como o Go sempre faz, e carregar os handles do navegador e da aba. Essa dupla função é o motivo de a configuração parecer do jeito que parece.
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var title string
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.Text("h1", &title, chromedp.NodeVisible),
)
O primeiro NewContext aloca um navegador. Contexts seguintes derivados dele criam novas abas no mesmo navegador, que é como você roda várias páginas sem pagar o custo de partida do navegador repetidamente:
browserCtx, cancelBrowser := chromedp.NewContext(context.Background())
defer cancelBrowser()
tabCtx, cancelTab := chromedp.NewContext(browserCtx)
defer cancelTab()
Cancelar fecha as coisas. Cancelar um context de aba fecha a aba; cancelar o context do navegador fecha o navegador. O defer cancel() não é contabilidade opcional — omiti-lo vaza um processo Chrome.
Timeouts se compõem do jeito ordinário do Go:
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
Dois erros do próprio FAQ do projeto valem conhecer de antemão.
"Executing an action without Run results in 'invalid context'." O FAQ explica que "by default, a chromedp context does not have an executor, however one can be specified manually if necessary". Actions não se executam sozinhas — são valores que o Run realiza.
"I'm seeing 'context canceled' errors." O FAQ atribui isso a perder a conexão: "when the connection to the browser is lost, chromedp cancels the context, and it may result in this error. This occurs, for example, if the browser is closed manually, or if the browser process has been killed or otherwise terminated." Então um erro context canceled frequentemente significa que o Chrome morreu, não que o timeout disparou — vale distinguir antes de aumentar o timeout.
Actions
Run recebe uma sequência de actions e as executa em ordem. As comuns cobrem a maior parte do trabalho.
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com/search"),
chromedp.WaitVisible(`input[name="q"]`),
chromedp.SendKeys(`input[name="q"]`, "golang"),
chromedp.Click(`button[type="submit"]`, chromedp.NodeVisible),
chromedp.WaitVisible(`.results`),
chromedp.Text(`.results`, &results, chromedp.NodeVisible),
)
Extrair de vários elementos usa Nodes ou Evaluate:
var links []string
err := chromedp.Run(ctx,
chromedp.Navigate(url),
chromedp.Evaluate(`[...document.querySelectorAll('a')].map(a => a.href)`, &links),
)
Evaluate executa JavaScript na página e desempacota o resultado num valor Go, que frequentemente é o caminho mais curto para qualquer coisa envolvendo vários elementos de uma vez. O valor precisa ser serializável em JSON.
Para actions que devolvem vários valores, o FAQ dá o wrapper:
chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error {
_, err := domain.SomeAction().Do(ctx)
return err
}))
ActionFunc também é como você desce para chamadas cruas de cdproto para qualquer coisa que a API de alto nível não cubra — definir cookies, interceptar requisições de rede, emular um dispositivo. Essa escotilha está disponível para o DevTools Protocol inteiro, que é uma superfície grande.
Esperar do jeito certo
A diferença entre um scraper confiável e um instável, e o erro é sempre o mesmo.
Não faça sleep. chromedp.Sleep(3*time.Second) existe, é tentador, e ou é curto demais — produzindo falhas intermitentes num dia lento — ou longo demais, desperdiçando tempo em cada execução. Em geral os dois, em máquinas diferentes.
Espere a coisa com que você se importa:
chromedp.WaitVisible(`.results`, chromedp.ByQuery)
chromedp.WaitNotVisible(`.spinner`)
chromedp.WaitReady(`#content`)
WaitVisible espera o elemento existir e estar visível; WaitReady espera ele existir no DOM. Para conteúdo carregado depois de uma interação, visível costuma ser a condição certa.
Para uma condição que nenhum seletor expressa, faça poll na página:
chromedp.Poll(`document.querySelectorAll('.item').length >= 20`, nil)
Essa é a resposta para "espere até a lista terminar de carregar", que nenhuma espera baseada em elemento consegue expressar.
Sempre limite a espera com um timeout de context. Um WaitVisible num seletor que nunca vai casar bloqueia até o context expirar, e sem timeout isso é para sempre.
Rodar headless, e no Docker
O Chrome roda headless por padrão. O FAQ responde a primeira pergunta que as pessoas têm: "By default, Chrome is run in headless mode. See DefaultExecAllocatorOptions, and an example to override the default options."
Para ver ele trabalhar enquanto desenvolve:
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", false),
)
allocCtx, cancelAlloc := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancelAlloc()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
NewExecAllocator é onde você define as flags de linha de comando do Chrome, e é a camada acima do context do navegador.
Para containers, a recomendação do projeto é específica: "The simplest way is to run the Go program that uses chromedp inside the chromedp/headless-shell image. That image contains headless-shell, a smaller headless build of Chrome, which chromedp is able to find out of the box."
Vale seguir. Montar um Chrome funcionando num container na mão significa caçar bibliotecas compartilhadas e pacotes de fonte faltando, e o resultado é maior do que a imagem feita para isso.
Um comportamento específico de Linux do FAQ, que surpreende quem roda o Chrome separado: "On Linux, chromedp is configured to avoid leaking resources by force-killing any started Chrome child processes. If you need to launch a long-running Chrome instance, manually start Chrome and connect using RemoteAllocator."
RemoteAllocator conecta a um navegador já em execução pelo endpoint websocket, que é o padrão para um pool compartilhado de navegadores ou um navegador rodando num container separado.
Usar um proxy
Três linhas, e uma pegadinha.
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.ProxyServer("http://proxy.example.com:9000"),
)
allocCtx, cancelAlloc := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancelAlloc()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
ProxyServer define a flag --proxy-server do Chrome.
A pegadinha é autenticação. A flag --proxy-server do Chrome não aceita credenciais — uma URL com usuário e senha nela não autentica. O Chrome, em vez disso, responde a um desafio de proxy mostrando um diálogo, e um navegador headless não tem ninguém para preenchê-lo.
Dois jeitos de contornar, em ordem de preferência.
Use uma allowlist de IP. Se o endereço de origem for estável, registre-o no provedor e descarte as credenciais. É a resposta mais limpa e remove o problema em vez de contorná-lo.
Trate o evento de autenticação. chromedp pode responder ao pedido de autenticação do DevTools Protocol via fetch.Enable com handleAuthRequests, fornecendo credenciais por programa. É mais código, e é o caminho quando o endereço não é fixo.
Depois verifique se funcionou, porque um proxy mal configurado num navegador é silencioso:
var ip string
err := chromedp.Run(ctx,
chromedp.Navigate("https://api.ipify.org"),
chromedp.Text("body", &ip, chromedp.NodeVisible),
)
Rode com e sem a opção de proxy. Se o endereço não mudar, o Chrome não está usando — e nada terá te avisado. Para trabalho geo-direcionado, vá além e confirme que conteúdo regionalmente distinto de fato difere, já que o endereço é a parte fácil. Esse é o padrão de falha silenciosa que descrevemos em por que testar proxies importa.
Case o locale com o país de saída enquanto estiver nisso. Uma saída alemã com um header de idioma en-US e fuso de Londres é uma combinação que nenhum visitante real produz, e muitos sites usam locale independentemente do endereço:
chromedp.Flag("lang", "de-DE"),
Cortar largura de banda
A seção que mais economiza dinheiro, e vale para qualquer automação de navegador.
Uma página que é 200 KB de HTML pode ser 4 MB quando cada imagem, fonte, script de rastreamento e preload de vídeo foi buscado. Nas tarifas residenciais de US$ 0,79/GB — nossos números, conferidos na página de preços em setembro de 2026 — essa diferença é o orçamento inteiro.
Bloqueie tipos de recurso de que você não precisa. Usando fetch.Enable e um listener de request-paused, aborte requisições de imagens, mídia e fontes:
chromedp.ListenTarget(ctx, func(ev interface{}) {
if e, ok := ev.(*fetch.EventRequestPaused); ok {
go func() {
c := chromedp.FromContext(ctx)
execCtx := cdp.WithExecutor(ctx, c.Target)
switch e.ResourceType {
case network.ResourceTypeImage, network.ResourceTypeMedia, network.ResourceTypeFont:
_ = fetch.FailRequest(e.RequestID, network.ErrorReasonBlockedByClient).Do(execCtx)
default:
_ = fetch.ContinueRequest(e.RequestID).Do(execCtx)
}
}()
}
})
Isso rotineiramente corta a maior parte do tráfego, e acelera as execuções como efeito colateral.
Reutilize o navegador, crie abas. A partida do navegador é cara; uma aba é barata. Para uma execução em muitas páginas, aloque uma vez e derive contexts de aba.
E pergunte se você precisa de um navegador. Se o conteúdo está no HTML inicial, uma requisição HTTP simples custa uma fração e roda bem mais rápido. Confira o código-fonte da página antes de recorrer à automação — o reflexo de renderizar tudo é a fonte mais comum de custo desnecessário nessa área.
Depurar quando nada funciona
Automação de navegador falha de forma opaca — um seletor que nunca casa e uma página que nunca carregou produzem o mesmo timeout. Uma sequência fixa resolve a maior parte.
Ligue o navegador e olhe. O diagnóstico mais rápido, e o que as pessoas deixam por último:
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", false),
)
Metade das vezes a resposta aparece na hora — um banner de cookie cobrindo o botão, um redirecionamento para login, uma tela de desafio, ou um layout diferente do que você testou.
Capture a página quando uma execução falha. Num ambiente headless isso substitui olhar:
var buf []byte
_ = chromedp.Run(ctx, chromedp.FullScreenshot(&buf, 90))
_ = os.WriteFile("failure.png", buf, 0644)
Emparelhe com o HTML, já que um screenshot mostra o que renderizou e o fonte mostra o que chegou:
var html string
_ = chromedp.Run(ctx, chromedp.OuterHTML("html", &html, chromedp.ByQuery))
Confira se o seletor resolve antes de assumir um problema de timing:
var count int
_ = chromedp.Run(ctx, chromedp.Evaluate(`document.querySelectorAll('.item').length`, &count))
Zero significa problema de seletor, e nenhuma quantidade de espera conserta.
Ative o log do navegador quando suspeitar que a própria página está errando:
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("enable-logging", true),
chromedp.Flag("v", "1"),
)
Escute mensagens de console e requisições que falharam, que frequentemente explicam uma página que renderiza vazia:
chromedp.ListenTarget(ctx, func(ev interface{}) {
switch e := ev.(type) {
case *runtime.EventConsoleAPICalled:
log.Printf("console.%s", e.Type)
case *network.EventLoadingFailed:
log.Printf("failed: %s %s", e.Type, e.ErrorText)
}
})
Uma página cujas chamadas de API estão todas falhando parece idêntica a uma página cujos seletores mudaram, e só os eventos de rede as distinguem.
E use chromedp-proxy como último recurso. Ele fica entre o seu programa e o navegador e registra o tráfego do DevTools Protocol nos dois sentidos. Quando o comportamento não faz sentido nenhum, ver a troca real de protocolo costuma explicar numa leitura.
Quando o chromedp é a escolha certa
Use quando você já está escrevendo Go e quer um único binário estático sem driver para distribuir, ou quando precisa de acesso direto ao DevTools Protocol para algo que as ferramentas de nível mais alto não expõem.
Considere Playwright quando quiser suporte multi-navegador, auto-espera embutida em cada action, tracing e screenshots na falha, ou um corpo maior de documentação e exemplos. O port para Go existe, mas o ecossistema é centrado em JavaScript e Python.
Considere HTTP simples quando o conteúdo está na resposta inicial. Mais rápido, mais barato, mais simples, e mais da web serve HTML útil do que o discurso sugere.
A lista de recursos do próprio FAQ é um bom mapa do próximo passo: o repositório examples para actions complexas e screenshots de página inteira, a referência cdproto para a API de protocolo gerada, e chromedp-proxy — um proxy de log CDP — para ver exatamente o que seu programa e o navegador estão dizendo um ao outro, que é a ferramenta de depuração de último recurso e uma genuinamente boa.
Perguntas frequentes
O que é o chromedp?
Um pacote Go que controla navegadores que falam o Chrome DevTools Protocol, sem dependências externas. Diferente do Selenium, não precisa de binário de driver; diferente do Playwright, não envia runtime — um binário Go mais uma instalação do Chrome é o deploy inteiro.
Por que eu recebo "invalid context" no chromedp?
Porque você executou uma action sem Run. O FAQ explica que um context do chromedp não tem executor por padrão. Actions são valores que o chromedp.Run realiza; chamar uma diretamente não tem nada para executá-la.
O que "context canceled" significa no chromedp?
Em geral que a conexão com o navegador se perdeu. O FAQ atribui ao navegador ser fechado manualmente ou o processo ser morto. Vale distinguir de um timeout, porque o conserto é diferente — um Chrome que crashou não se resolve esperando mais.
Como eu rodo o chromedp com um navegador visível?
O Chrome roda headless por padrão. Acrescente chromedp.Flag("headless", false) a DefaultExecAllocatorOptions e passe para NewExecAllocator, depois derive seu context desse allocator.
Como eu uso um proxy com o chromedp?
Adicione chromedp.ProxyServer("http://host:port") às opções do allocator. Credenciais na URL não funcionam, porque a flag --proxy-server do Chrome não as aceita — use uma allowlist de IP se o endereço for estável, ou trate o pedido de autenticação pelo DevTools Protocol.
Como eu rodo o chromedp no Docker?
Rode o programa Go dentro da imagem chromedp/headless-shell, que o projeto recomenda explicitamente. Ela contém um build headless menor do Chrome que o chromedp encontra sem configuração, e evita montar um ambiente de navegador na mão.
Como eu espero um elemento no chromedp?
Use WaitVisible, WaitReady ou WaitNotVisible com um seletor, ou Poll com uma expressão JavaScript para condições que nenhum seletor expressa. Evite Sleep — ou é curto demais e instável, ou longo demais e desperdício, em geral os dois conforme a máquina.
Como eu reduzo banda ao usar chromedp?
Bloqueie imagens, fontes e mídia habilitando interceptação de requisição e falhando esses tipos de recurso, o que em geral remove a maior parte do tráfego. Reutilize um navegador e crie abas em vez de alocar repetidamente. E confira se o conteúdo está no HTML inicial, caso em que pule o navegador por completo.
Conclusão
A curva de aprendizado do chromedp é quase inteiramente o modelo de context. Depois que você internaliza que um context carrega o navegador ou a aba, que cancelar um fecha, e que actions não fazem nada até o Run realizá-las, o resto da API é direto.
Os hábitos que importam são os mesmos de qualquer automação de navegador. Espere condições em vez de dormir, limite cada espera com um timeout de context, e reutilize um navegador em muitas abas em vez de pagar o custo de partida repetidamente.
Dois pontos específicos de Go valem lembrar. defer cancel() em cada context, ou você vaza processos Chrome — e no Linux, o chromedp mata à força os filhos Chrome que iniciou, então um navegador de longa duração precisa ser lançado à parte e alcançado com RemoteAllocator.
E se você está rodando por um proxy medido, bloqueie os tipos de recurso de que não precisa antes de qualquer outra coisa. Uma página renderizada custa uma ordem de magnitude a mais do que o HTML que contém, e a maior parte disso são imagens que você nunca ia olhar.
