La rotación de proxies en Colly es la técnica número uno para mantener scrapers en Go funcionando a escala sin que los targets te bloqueen. Si llegas aquí con un scraper que funciona en local pero muere tras 100 peticiones en producción, este artículo es para ti. Vamos a cubrir desde el modelo de collector de Colly hasta la implementación idiomática de proxy switchers con proxies residenciales de ProxyHat, pasando por patrones de producción como reintentos, concurrencia y scraping distribuido.
Aviso legal: Esta guía asume que extraes datos públicos respetando los términos de servicio de cada sitio, el archivo
robots.txty normativas como el RGPD (Reglamento General de Protección de Datos) en la UE y la CFAA (Computer Fraud and Abuse Act) en EE. UU. No uses estas técnicas para acceder a datos protegidos por autenticación sin autorización.
Rotación de proxies en Colly: por qué y cómo funciona
Colly es el framework de scraping más popular en Go, con más de 24,000 estrellas en GitHub. Su diseño se basa en un modelo de collector que gestiona peticiones HTTP, parseo HTML via goquery, y callbacks encadenados. La rotación de proxies en Colly se integra de forma nativa a través de dos mecanismos: proxy.RoundRobinProxySwitcher para rotación simple, y c.SetProxyFunc para lógica personalizada que puede incluir geo-targeting, sesiones sticky y selección inteligente de IPs.
El problema es simple: los sitios modernos usan WAFs (Web Application Firewalls) como Cloudflare o Akamai que detectan y bloquean IPs que generan patrones anómalos de tráfico. Una sola IP datacenter haciendo 500 peticiones/minuto a un e-commerce es una bandera roja inmediata. La solución es distribuir esas peticiones entre cientos o miles de IPs residenciales, cada una con un perfil de tráfico normal.
El modelo de Collector de Colly
Antes de meter proxies, necesitas entender cómo funciona un Collector en Colly. El Collector es el objeto central que orquesta todo:
- OnRequest(func(*colly.Request)) — se ejecuta antes de cada petición. Aquí puedes modificar headers, añadir cookies, o loggear.
- OnHTML(selector, func(*colly.HTMLElement)) — callback por selector CSS. Usa goquery por debajo para recorrer el DOM.
- OnError(func(*colly.Response, error)) — captura errores HTTP y de red. Es donde implementas reintentos.
- OnResponse(func(*colly.Response)) — se ejecuta tras recibir la respuesta, antes del parseo HTML.
El collector soporta modo asíncrono con c.Async = true, lo que permite disparar peticiones concurrentes. Esto es clave para alto throughput, pero requiere configurar Limit() para evitar saturar el target o tus proxies.
Tipos de proxy y cuándo usar cada uno
La elección del tipo de proxy determina tu success rate, latencia y costo. Esta tabla resume las diferencias:
| Característica | Datacenter | Residencial | Mobile |
|---|---|---|---|
| Velocidad media | 50-100ms | 200-800ms | 500-2000ms |
| Success rate en targets duros | 30-50% | 90-98% | 95-99% |
| Detección por WAF | Alta | Baja | Muy baja |
| Costo relativo | $ | $$ | $$$ |
| Casos de uso | APIs públicas, datos abiertos | E-commerce, SERP, social media | Login flows, verificación SMS |
Para la mayoría de proyectos de scraping en Go con Colly, los proxies residenciales ofrecen el mejor equilibrio. Puedes explorar las ubicaciones disponibles de ProxyHat para elegir el país o ciudad que necesitas.
Implementación idiomática: RoundRobinProxySwitcher
Colly incluye un proxy switcher round-robin en el paquete colly/proxy. Es la forma más simple de rotación de proxies en Colly: le pasas una lista de URLs de proxy y él va rotando secuencialmente. Aquí tienes un ejemplo funcional con ProxyHat:
package main
import (
"log"
"github.com/gocolly/colly/v2"
"github.com/gocolly/colly/v2/proxy"
)
func main() {
// Lista de proxies ProxyHat con geo-targeting por país
proxyURLs := []string{
"http://user-country-US:pass@gate.proxyhat.com:8080",
"http://user-country-DE:pass@gate.proxyhat.com:8080",
"http://user-country-GB:pass@gate.proxyhat.com:8080",
"http://user-country-FR:pass@gate.proxyhat.com:8080",
"http://user-country-JP:pass@gate.proxyhat.com:8080",
}
rp, err := proxy.RoundRobinProxySwitcher(proxyURLs...)
if err != nil {
log.Fatal(err)
}
c := colly.NewCollector()
c.SetProxyFunc(rp)
c.OnRequest(func(r *colly.Request) {
log.Printf("Visitando %s via proxy", r.URL.String())
})
c.OnHTML("title", func(e *colly.HTMLElement) {
log.Printf("Título: %s", e.Text)
})
c.Visit("https://httpbin.org/ip")
c.Wait()
}
El RoundRobinProxySwitcher rota la IP en cada petición. Como ProxyHat asigna una IP residencial diferente por conexión cuando usas geo-targeting por país, cada petición sale desde una IP distinta dentro del país especificado. Esto significa que con 5 URLs de proxy, estás efectivamente rotando entre 5 pools de IPs residenciales.
Limitaciones del RoundRobinProxySwitcher
El switcher round-robin tiene dos limitaciones importantes:
- No soporta lógica condicional — no puedes elegir un proxy distinto según el dominio o el tipo de petición.
- No maneja fallos de proxy — si un proxy devuelve error, el switcher sigue usándolo en su turno. No hay circuit breaker.
Para escenarios más complejos, necesitas un SetProxyFunc personalizado.
SetProxyFunc: rotación personalizada con geo-targeting
El método c.SetProxyFunc(func(*http.Request) (*url.URL, error)) te da control total sobre qué proxy usar para cada petición. Aquí es donde la rotación de proxies en Colly brilla de verdad: puedes combinar geo-targeting, sesiones sticky y selección inteligente.
ProxyHat soporta geo-targeting y sesiones directamente en el username del proxy. Por ejemplo:
user-country-DE-city-berlin:pass— IP residencial en Berlín, Alemania.user-session-abc123:pass— IP sticky mantenida durante la sesiónabc123.user-country-US-session-myapp42:pass— IP sticky en EE. UU. para la sesiónmyapp42.
Aquí tienes un switcher personalizado que rota países y genera sesiones únicas:
package main
import (
"crypto/rand"
"encoding/hex"
"log"
"math/rand"
"net/http"
"net/url"
"sync/atomic"
"github.com/gocolly/colly/v2"
)
var countries = []string{"US", "DE", "GB", "FR", "JP", "CA", "AU"}
var counter uint64
func customProxyFunc(req *http.Request) (*url.URL, error) {
idx := atomic.AddUint64(&counter, 1)
country := countries[idx%uint64(len(countries))]
// Generar sesión única para rotar IP en cada petición
sessionBytes := make([]byte, 8)
rand.Read(sessionBytes)
session := hex.EncodeToString(sessionBytes)
proxyStr := fmt.Sprintf(
"http://user-country-%s-session-%s:pass@gate.proxyhat.com:8080",
country, session,
)
return url.Parse(proxyStr)
}
func main() {
c := colly.NewCollector()
c.SetProxyFunc(customProxyFunc)
c.OnRequest(func(r *colly.Request) {
r.Headers.Set("User-Agent", "Mozilla/5.0 (compatible; MyBot/1.0)")
})
c.OnHTML("body", func(e *colly.HTMLElement) {
log.Printf("IP detectada en %s", e.Text)
})
c.Visit("https://httpbin.org/ip")
c.Wait()
}
Con este enfoque, cada petición obtiene una IP residencial nueva en un país rotatorio. La sesión aleatoria fuerza a ProxyHat a asignar una IP distinta cada vez. Si necesitas mantener la misma IP entre peticiones (por ejemplo, para un flujo de login), reemplaza la sesión aleatoria por un ID fijo.
Ejemplo completo: Collector con Limit, reintentos y residenciales
Aquí tienes un ejemplo de producción que combina rotación de proxies residenciales, control de concurrencia, delays aleatorios y reintentos con c.Clone():
package main
import (
"fmt"
"log"
"math/rand"
"net/http"
"net/url"
"sync/atomic"
"time"
"github.com/gocolly/colly/v2"
"github.com/gocolly/colly/v2/debug"
)
var countries = []string{"US", "DE", "GB", "FR", "JP"}
var reqCounter uint64
func proxySwitcher(req *http.Request) (*url.URL, error) {
idx := atomic.AddUint64(&reqCounter, 1)
country := countries[idx%uint64(len(countries))]
session := fmt.Sprintf("sess-%d-%d", time.Now().Unix(), idx)
proxyStr := fmt.Sprintf(
"http://user-country-%s-session-%s:YOURPASS@gate.proxyhat.com:8080",
country, session,
)
return url.Parse(proxyStr)
}
func main() {
c := colly.NewCollector(
colly.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"),
colly.Async(true),
)
c.SetProxyFunc(proxySwitcher)
// Limitar concurrencia y añadir delay aleatorio
c.Limit(&colly.LimitRule{
DomainGlob: "*",
Parallelism: 10,
RandomDelay: 2 * time.Second,
})
// Configurar timeout en el Transport
c.WithTransport(&http.Transport{
Timeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 15 * time.Second,
})
var retryCount = make(map[string]int)
var maxRetries = 3
c.OnError(func(r *colly.Response, err error) {
urlStr := r.Request.URL.String()
if retryCount[urlStr] < maxRetries {
retryCount[urlStr]++
log.Printf("Reintentando %s (intento %d/%d): %v",
urlStr, retryCount[urlStr], maxRetries, err)
time.Sleep(time.Duration(1+rand.Intn(3)) * time.Second)
r.Request.Retry()
} else {
log.Printf("Falló definitivamente %s: %v", urlStr, err)
}
})
c.OnHTML("h1", func(e *colly.HTMLElement) {
log.Printf("Título H1: %s", e.Text)
})
// Encolar URLs
urls := []string{
"https://example.com/page1",
"https://example.com/page2",
"https://example.com/page3",
}
for _, u := range urls {
c.Visit(u)
}
c.Wait()
}
Este ejemplo usa Parallelism: 10 con un RandomDelay de 2 segundos, lo que significa un máximo de 10 peticiones concurrentes con un jitter aleatorio entre ellas. El OnError reintenta hasta 3 veces con backoff aleatorio antes de declarar fallo definitivo.
SOCKS5 vs HTTP: cuándo usar cada uno
ProxyHat soporta tanto HTTP como SOCKS5. Para la mayoría de casos de colly web scraping, HTTP es suficiente. SOCKS5 es útil cuando:
- Necesitas tunelizar tráfico que no es HTTP (raro en Colly).
- El target bloquea conexiones que parecen usar proxies HTTP (algunos WAFs detectan el header
ViaoX-Forwarded-Foren proxies HTTP mal configurados). - Quieres evitar cualquier leak de IP en redirects.
Para usar SOCKS5 con ProxyHat, simplemente cambia el puerto a 1080 y el esquema:
proxyStr := "socks5://user-country-DE-session-abc123:pass@gate.proxyhat.com:1080"
Colly maneja SOCKS5 nativamente a través del Transport estándar de Go, así que SetProxyFunc funciona sin cambios adicionales.
Patrones de producción para alto volumen
Reintentos con c.Clone()
El método c.Clone() crea una copia del collector con la misma configuración. Es útil para reintentos porque te permite crear un collector hijo con un proxy distinto sin afectar al principal:
c.OnError(func(r *colly.Response, err error) {
if retryCount[r.Request.URL.String()] < maxRetries {
retryCount[r.Request.URL.String()]++
// Clonar collector para reintentar con nueva IP
clone := r.Request.C.Clone()
clone.SetProxyFunc(proxySwitcher) // Nueva IP
clone.Visit(r.Request.URL.String())
}
})
Scraping distribuido con Redis
Para volúmenes de millones de URLs, una sola instancia no basta. Colly soporta storage backends como Redis para coordinar múltiples workers. El patrón es:
- Usar
redisstorage.New()como storage del collector. - Compartir la cola de URLs visitadas entre instancias.
- Cada worker corre en su propio contenedor con su propio pool de proxies.
import "github.com/gocolly/redisstorage"
storage := &redisstorage.Storage{
Address: "redis:6379",
Password: "",
Prefix: "my_scraper",
Client: nil,
}
c.SetStorage(storage)
// Las URLs visitadas se comparten entre instancias
// evitando duplicados en crawling distribuido
Containerización y escalado horizontal
Para escalar, ejecuta múltiples instancias del scraper en contenedores Docker o Kubernetes. Cada instancia debe tener:
- Su propio set de sesiones proxy para evitar colisiones.
- Límites de concurrencia ajustados al plan de ProxyHat.
- Health checks que monitoricen el success rate y reinicien el pod si cae por debajo del 80%.
Consulta la documentación de ProxyHat para conocer los límites de concurrencia de tu plan y las opciones de pricing disponibles.
Trampas comunes y edge cases
1. Olvidar c.Wait() en modo Async
Con c.Async = true, las peticiones son no bloqueantes. Si tu main() termina antes de que las peticiones completen, perderás datos. Siempre llama c.Wait() al final.
2. No configurar timeouts en el Transport
El Transport por defecto de Go tiene timeouts generosos. Un proxy residencial que se cuelga puede bloquear un slot de concurrencia por minutos. Configura siempre:
Timeout: 30s máximo.TLSHandshakeTimeout: 10s.ResponseHeaderTimeout: 15s.
3. Reutilizar sesiones cuando necesitas rotación
Si usas user-session-fixed:pass en todas las peticiones, obtendrás la misma IP. Esto es útil para flujos de login, pero contraproducente para rotación. Genera sesiones únicas con crypto/rand para forzar IPs nuevas.
4. Ignorar el Content-Type
Algunos endpoints devuelven JSON en lugar de HTML. Colly solo dispara OnHTML si el Content-Type es text/html. Para JSON, usa OnResponse y parsea manualmente.
5. No respetar robots.txt
Colly ignora robots.txt por defecto. Puedes activar el chequeo con colly.AllowURLRevisit() y lógica custom, o simplemente sé disciplinado: no raspes endpoints marcados como Disallow. Más información sobre ética en scraping en nuestra guía de casos de uso de web scraping.
Cuándo NO usar Colly
Colly es excelente para HTML estático y APIs JSON, pero no ejecuta JavaScript. Si el target es una SPA (Single Page Application) que renderiza contenido via React, Vue o Angular en el cliente, Colly no verá el contenido dinámico. En ese caso necesitas un navegador headless:
- Chromedp — librería Go para controlar Chrome via DevTools Protocol. Puedes combinarla con proxies SOCKS5 de ProxyHat.
- Playwright/Rod — alternativas con mejor soporte para multi-browser.
- Browserless — servicio gestionado que ejecuta Chrome en Docker con API REST.
Para casos de uso como SERP tracking donde Google renderiza resultados dinámicamente, un navegador headless con proxies residenciales puede ser necesario. Colly funciona para la mayoría de SERPs tradicionales, pero si ves contenido faltante, considera migrar a Chromedp.
Key Takeaways
- RoundRobinProxySwitcher es el punto de partida más simple para rotación de proxies en Colly. Úsalo para casos básicos con 3-10 IPs.
- SetProxyFunc personalizado es la solución idiomática para geo-targeting, sesiones sticky y lógica condicional. Es donde ProxyHat brilla con usernames dinámicos.
- Proxies residenciales con geo-targeting por país (
user-country-DE) ofrecen success rates del 90-98% en targets restrictivos, frente al 30-50% de datacenter. - Limit() con RandomDelay es esencial para no saturar el target. 10 peticiones concurrentes con 2s de delay es un buen punto de partida.
- OnError con c.Clone() permite reintentar con IPs nuevas sin afectar al collector principal.
- Redis storage habilita scraping distribuido entre múltiples contenedores, compartiendo la cola de URLs visitadas.
- Colly no ejecuta JS — si el target es una SPA, migra a Chromedp o Rod con proxies SOCKS5 de ProxyHat en el puerto 1080.
FAQ
¿Qué es la rotación de proxies en Colly?
La rotación de proxies en Colly consiste en cambiar la IP de salida en cada petición HTTP usando proxy.RoundRobinProxySwitcher o una función personalizada con c.SetProxyFunc. Esto distribuye las solicitudes entre múltiples IPs para evitar bloqueos por rate-limiting o detección de bots.
¿Por qué importa la rotación de proxies en Colly?
Los sitios modernos detectan y bloquean IPs que generan muchas peticiones. Sin rotación, un scraper en Colly puede ser bloqueado tras 50-200 peticiones. Rotar proxies residenciales permite mantener success rates superiores al 95% distribuyendo el tráfico entre miles de IPs reales.
¿Qué tipo de proxy funciona mejor para la rotación en Colly?
Los proxies residenciales son los más efectivos para targets restrictivos porque usan IPs reales de ISPs. Los datacenter proxies son más rápidos pero fácilmente detectados. Los mobile proxies ofrecen máxima confianza pero a mayor costo. Para alto volumen, residenciales con geo-targeting son el equilibrio ideal.
¿Cómo evitas bloqueos al implementar rotación de proxies en Colly?
Combina rotación de IPs residenciales con delays aleatorios (RandomDelay), límites de concurrencia con Limit(), reintentos con c.Clone() en OnError, sesiones sticky cuando sea necesario, y respeto a robots.txt. Configura timeouts en el Transport y usa Redis para distribuir entre instancias.






