Sincronizar cincuenta contactos con la API de GoHighLevel es un tutorial. Sincronizar cincuenta mil es un proyecto de ingeniería, y la diferencia no está en el bucle: está en todo lo que el bucle ingenuo no contempla: los límites de peticiones que convierten horas en la unidad de medida, los timeouts de las funciones serverless que matan el proceso a la mitad, las corridas dobles que duplican datos, y los errores intermitentes que a volumen dejan de ser excepción para volverse estadística. Este artículo documenta la arquitectura de una sincronización masiva real en producción, con sus números: unos 8 contactos por segundo sostenidos, en lotes de 400, con reanudación automática y sin duplicar jamás.
El contexto del patrón: una base externa (el CRM de origen, una migración, un sistema propio) debe volcarse o mantenerse espejada contra GoHighLevel. Lo que sigue vale para la carga inicial grande y para la sincronización periódica que la mantiene viva después.
Respuesta rápida
- La aritmética primero: con los límites de la API (ráfagas de 100 peticiones por 10 segundos), el techo práctico ronda los 8-10 contactos por segundo: 50.000 contactos son horas de proceso, y el diseño debe asumirlo.
- Lotes con estado: el trabajo se parte en lotes (400 funciona bien) y el progreso se guarda tras cada uno: cualquier muerte del proceso reanuda desde el último lote confirmado.
- Auto-encadenamiento: en serverless, cada ejecución procesa su lote y se invoca a sí misma para el siguiente, esquivando los límites de tiempo de ejecución.
- Idempotencia por upsert: cada contacto se escribe por identidad (email/teléfono vía upsert), no por inserción: repetir un lote no duplica nada.
- Candado optimista: una marca de «corrida en curso» impide que dos sincronizaciones simultáneas se pisen.
- Backoff tipado: el 429 espera y reintenta; el 400 registra y continúa; el 500 reintenta con espera creciente. Cada tipo de error tiene su política, no un try/catch genérico.
| Problema a escala | Síntoma sin diseño | Pieza que lo resuelve |
|---|---|---|
| Rate limits | Lluvia de 429 y proceso bloqueado | Throttling a ritmo sostenible + backoff |
| Timeout serverless | Proceso muerto al minuto N, sin rastro | Lotes + auto-encadenamiento |
| Corrida interrumpida | Reempezar desde cero (y duplicar) | Checkpoint por lote + upsert |
| Dos corridas a la vez | Carreras y datos pisados | Candado optimista |
| Errores intermitentes | Un contacto malo tumba el lote entero | Política por tipo de error |
Cinco problemas, cinco piezas. Ninguna es exótica; la ingeniería está en tenerlas todas antes de que el volumen te las cobre.
¿Cómo sincronizar miles de contactos con la API de GoHighLevel?
Partiendo el trabajo en lotes con progreso guardado, escribiendo cada contacto por upsert (idempotente), respetando los límites de la API con un ritmo de unos 8 contactos por segundo, y encadenando ejecuciones serverless que se reanudan solas. Con candado contra corridas simultáneas y política de reintentos por tipo de error, 50.000 contactos se sincronizan en horas, sin duplicados.
La arquitectura, pieza por pieza
1. La aritmética que define todo lo demás
Los límites publicados de la API v2 (100 peticiones por ráfaga de 10 segundos, 200.000 diarias por subcuenta) fijan el techo teórico en 10 por segundo. En la práctica, entre latencias y márgenes de seguridad, el ritmo sostenible ronda los 8. Esa cifra convierte cualquier promesa de «migramos esta tarde» en matemática: 10.000 contactos ≈ 20 minutos; 50.000 ≈ 2 horas; 200.000 ≈ un día con presupuesto diario incluido. Diseñar sin esta aritmética es como planear un viaje sin saber que el coche tiene velocidad máxima: todo el plan hereda el error.
2. Lotes con checkpoint: la unidad de reanudación
El proceso nunca es «un bucle de 50.000»: son 125 lotes de 400, y tras cada lote se persiste el progreso (último procesado, contadores, errores) en la base de control. La consecuencia es la propiedad que separa producción de tutorial: el proceso puede morir en cualquier momento sin perder nada. Deploy a mitad de corrida, función expirada, red caída: la siguiente ejecución lee el checkpoint y continúa desde el lote siguiente, no desde cero. El tamaño de 400 no es sagrado: es el equilibrio que encontramos entre eficiencia de proceso y granularidad de reanudación, y cualquier valor entre 200 y 500 funciona si el checkpoint acompaña.
3. Auto-encadenamiento serverless
Las funciones serverless tienen límites de ejecución que una sincronización de horas revienta. El patrón que lo resuelve: cada invocación procesa UN lote (minutos, cómodamente dentro del límite) y, antes de terminar, se invoca a sí misma para el siguiente. La «corrida» deja de ser un proceso largo y se vuelve una cadena de procesos cortos, cada uno barato, reanudable y dentro de los límites de la plataforma. Sobre la misma base de Supabase que ya juega otros papeles en la arquitectura, las edge functions encadenadas hacen este trabajo sin infraestructura adicional.
4. Upsert: la idempotencia que perdona todo
La regla de oro de cualquier proceso reanudable: repetirlo no puede duplicar. La API de contactos ofrece upsert por identidad (email o teléfono), y usarlo en lugar de crear a ciegas convierte cada reprocesamiento en inofensivo: el lote que se procesó una vez y media por una interrupción produce el mismo resultado que procesado una vez. Sin esta pieza, cada una de las anteriores (checkpoints, reanudación) sería una fábrica de duplicados con buenos modales.
5. El candado y los errores con apellido
Dos piezas de disciplina cierran el sistema. El candado optimista: antes de arrancar, la corrida marca «en curso» en la base de control y verifica que nadie más lo esté; el cron que dispara la sincronización nocturna mientras la carga inicial sigue viva se encuentra la puerta cerrada y se va. Y la política por tipo de error: el 429 (límite) espera con backoff creciente y reintenta; el 400 (dato malo: un email inválido, uno de los quirks documentados) registra el contacto en la lista de fallidos y sigue, porque un registro corrupto no puede secuestrar a los 49.999 restantes; el 500 reintenta un par de veces y luego registra. Al final, el reporte de corrida dice exactamente qué entró, qué falló y por qué: sin ese reporte, «terminó» es una palabra sin contenido.
Cuándo NO montar esto
Para una migración única de pocos miles de contactos, la importación por CSV de la propia plataforma es la herramienta correcta: robusta, con mapeo de campos y sin una línea de código. Este patrón se justifica cuando la sincronización es RECURRENTE (el espejo debe mantenerse vivo) o cuando el volumen y las transformaciones superan lo que el CSV expresa. Y para el flujo inverso (respaldar GoHighLevel hacia fuera), el patrón hermano ya está documentado en la guía de respaldos.
Errores comunes
- El bucle ingenuo: for contacto in lista: crear(contacto). Funciona en la demo de 50, muere en el minuto 3 de la corrida de 50.000, y deja mitad de trabajo sin saber cuál mitad.
- Crear en lugar de upsert: toda interrupción más reintento produce duplicados, y deduplicar después cuesta más que el proyecto original.
- Ignorar el presupuesto diario: 200.000 peticiones al día por subcuenta suenan infinitas hasta que la corrida grande compite con el resto de tus integraciones ese mismo día.
- Reintentar el 400: el dato malo no mejora con insistencia; reintentar errores permanentes solo quema límite y tiempo. Clasifica antes de reintentar.
- Sin reporte de fallidos: los 200 contactos que no entraron y nadie sabe cuáles son. La lista de fallidos con motivo es parte del entregable, no un lujo.
Preguntas frecuentes
A ritmo sostenible de unos 8 contactos por segundo, alrededor de 2 horas de proceso efectivo. Con validaciones, transformaciones y los márgenes del mundo real, planifica media jornada. Lo que no debería tardar es tu tiempo: bien montada, corre sola y reporta al terminar.
Sí: la escritura directa y masiva por API es exactamente el caso que requiere la API completa, disponible de Unlimited hacia arriba. Con Starter, la vía de importación CSV nativa cubre las migraciones únicas.
Se mapean por identificador de campo y se validan antes de enviar: el custom field con formato inesperado es la causa número uno de errores 400 a volumen. La regla de producción: normalizar y tipar ANTES de tocar la API, nunca confiar en que el origen viene limpio.
Sí, y el patrón maduro combina ambas: los webhooks empujan cambios individuales en tiempo real, y una corrida periódica (nocturna o semanal) reconcilia el conjunto completo contra el origen, atrapando lo que los eventos perdieron. Es el mismo principio push más pull de toda integración crítica.
Tres tablas humildes: corridas (estado, candado, contadores), checkpoints (progreso por lote) y fallidos (contacto, motivo, lote). Cualquier Postgres las sostiene; el valor no está en la infraestructura sino en que el proceso las use con disciplina.
Arquitectura verificada en una sincronización propia en producción moviendo decenas de miles de contactos contra la API v2, a julio de 2026. Si tienes una migración o un espejo de datos que ya te dio un susto, este es el tipo de sistema que construyo.
La idea de fondo
La sincronización masiva es el examen sorpresa de tu madurez de integración: revela si tus procesos asumen el mundo feliz o el mundo real. Las cinco piezas de este artículo (aritmética, lotes, encadenamiento, upsert, candado con errores tipados) son la misma decisión repetida: asumir que el proceso SE VA a interrumpir, que los datos SÍ vienen sucios y que los límites NO se negocian, y diseñar para que nada de eso importe. El bucle ingenuo apuesta a que todo saldrá bien. El sistema de producción garantiza que da igual si no.





