pg_cron en Supabase: El Patrón que Convierte Tu Base de Datos en un Orquestador Autônomo
Aprende a construir pipelines autonomous con pg_cron y pg_net en Supabase. Elimina polling y external schedulers. Guía práctica 2026.
El 90% de los Desarrolladores Usa pg_cron Solo Para Vacíos a las 3 AM
Vuestra base de datos Supabase lleva años haciendo una sola cosa con pg_cron: limpiar tablas temporales a medianoche.
Mientras tanto, estáis ignorando un orchestrator(event-driven) más potente que la mayoría de CI/CD schedulers que pagáis en servicios externos.
*El problema real no es que pg_cron no sirva para más. Es que estáis tratándolo como un timer cuando es un cerebro.*
La combinación de pg_cron + pg_net transforma Postgres en un sistema que no solo almacena datos — ejecuta pipelines completos, llama Edge Functions, y encadena trabajos de forma reactiva sin una capa de aplicación separada.
Por Qué Vuestros Cron Jobs Son Brite y Propensos a Fallar
Lo que hacéis la mayoría:
1. Programáis un cron job a las 3 AM
2. Ejecutáis una query de cleanup
3. Olvidáis el job hasta que alguien se queja de datos faltantes
Este enfoque tiene tres fallos catastróficos.
Primer fallo: Fire-and-forget sin observabilidad. Si el job falla, no os enteráis hasta horas después. No hay retry automático, no hay logs centralizados, no hay circuit breakers.
Segundo fallo: jobs independientes que no se comunican. Job A hace su trabajo. Job B hace el suyo. Si Job B necesita el output de Job A, dependéis de timing exacto en vez de dependencias reales.
Tercer fallo: polling everywhere. Cuando Job B necesita saber si Job A terminó, la solución más común es hacer que B pregunte cada 5 minutos hasta que A responda. Esto crea load spikesauto-infligidos y llena la base de datos de queries inútiles.
❌ ENFOQUE CONVENCIONAL (brite):
```
-- Job B polling a Job A cada 5 minutos
SELECT * FROM job_a_results WHERE completed = true;
-- 47 queries inútiles después...
-- Oh, ya terminó. Ahora ejecuto B.
```
✅ ENFOQUE REACTIVO (El Patrón de Cascada Autónoma):
```
-- Job A termina y hace trigger directo a Job B via pg_net
SELECT net.http_post(
'https://vuestro-proyecto.supabase.co/functions/v1/schedule-job-b',
json_build_object('trigger', 'job_a_complete'),
headers:= json_build_object('Authorization', 'Bearer ' || current_setting('app.service_role_key'))
);
```
pg_cron No Es Un Scheduler. Es Un Orquestador Event-Driven
pg_cron ejecuta como background worker dentro del proceso de Postgres. Zero network hops, zero autenticación externa, zero overhead de conexión.
Esto cambia la arquitectura.
Un scheduler externo (cron en una VM, cloud scheduler) necesita connection strings, SSL setup, y manejo de reconexiones. pg_cron corre en el mismo proceso que vuestra base de datos — hereda el security model naturalmente y elimina surface area para fallos.
La asimetría de pg_net que lo cambia todo:
La mayoría reach para LISTEN/NOTIFY cuando quieren patrones reactivos. pg_net llena otro nicho. Hace llamadas HTTP no-bloqueantes que devuelven inmediatamente un job_id — la función llamada completa sin esperar la respuesta HTTP.
Esto habilita patrones imposibles con NOTIFY solo:
→ Trigger a external APIs (Slack, Zapier, webhooks personalizados) desde dentro de un cron job
→ Llamar Edge Functions de Supabase que ellos mismos schedulan nuevos cron jobs
→ Crear autonomous feedback loops sin infraestructura adicional
El Framework de Cascada Autónoma: 5 Fases Para Pipelines Sin polling
Fase 1: Habilitad las Extensiones
Antes de nada, activad pg_cron y pg_net en vuestro proyecto Supabase:
```sql
-- Activar pg_cron (habilita scheduling dentro de Postgres)
CREATE EXTENSION IF NOT EXISTS pg_cron;
-- Activar pg_net (habilita HTTP requests asíncronos)
CREATE EXTENSION IF NOT EXISTS pg_net;
-- Verificar que están activas
SELECT * FROM pg_extension WHERE extname IN ('pg_cron', 'pg_net');
```
Fase 2: Diseñad Vuestro Pipeline Como DAG
Mapad vuestras jobs como un Directed Acyclic Graph. Cada job escribe su estado y resultados en una tabla de control que las jobs downstream pueden consultar.
```sql
-- Tabla de control para el pipeline
CREATE TABLE pipeline_control (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
job_name TEXT NOT NULL UNIQUE,
status TEXT NOT NULL CHECK (status IN ('pending', 'running', 'completed', 'failed')),
triggered_by TEXT,
result_json JSONB,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Job A inserta su estado
INSERT INTO pipeline_control (job_name, status, triggered_by)
VALUES ('job_a', 'running', NULL)
ON CONFLICT (job_name) DO UPDATE SET status = 'running', started_at = now();
```
Fase 3: Implementad Llamadas Reactivas con pg_net
En vez de polling, la job upstream hace trigger directo a la downstream:
```sql
-- Función que Job A ejecuta al completar
CREATE OR REPLACE FUNCTION notify_job_complete()
RETURNS void AS $$
DECLARE
service_role_key TEXT;
BEGIN
-- Obtener la key desdevault (no hardcodear!)
service_role_key := current_setting('app.service_role_key', true);
-- Trigger asíncrono a la Edge Function que schedula Job B
PERFORM net.http_post(
url := current_setting('app.edge_function_url') || '/schedule-job-b',
body := json_build_object(
'pipeline_id', current_setting('current_pipeline_id'),
'source_job', 'job_a'
)::jsonb,
headers := json_build_object(
'Content-Type', 'application/json',
'Authorization', 'Bearer ' || service_role_key
)
);
END;
$$ LANGUAGE plpgsql;
```
Fase 4: Instrumentad Observabilidad
pg_cron almacena todo en cron.job_run_details. Construid un dashboard básico:
```sql
-- Vista para monitorizar你们的 pipeline
CREATE OR REPLACE VIEW pipeline_health AS
SELECT
j.jobid,
j.schedule,
j.command,
r.status,
r.start_time,
r.end_time,
r.returncode,
EXTRACT(EPOCH FROM (r.end_time - r.start_time)) as duration_seconds
FROM cron.job j
LEFT JOIN LATERAL (
SELECT * FROM cron.job_run_details
WHERE jobid = j.jobid
ORDER BY start_time DESC
LIMIT 1
) r ON true
ORDER BY j.schedule;
```
Fase 5: Circuit Breakers y Retry Condicional
Cada job debe verificar el estado de la job anterior antes de ejecutarse:
```sql
-- Job B: solo ejecutar si Job A completó exitosamente
CREATE OR REPLACE FUNCTION job_b_check_and_run()
RETURNS void AS $$
DECLARE
job_a_status TEXT;
BEGIN
SELECT status INTO job_a_status
FROM pipeline_control
WHERE job_name = 'job_a';
IF job_a_status != 'completed' THEN
-- Alertar via pg_net antes de saltar
PERFORM net.http_post(
url := current_setting('app.slack_webhook_url'),
body := json_build_object(
'text', '⚠️ Pipeline bloqueado: job_a no completó. Job B saltado.'
)::jsonb
);
RETURN;
END IF;
-- Ejecutar job B
PERFORM job_b_execute();
END;
$$ LANGUAGE plpgsql;
```
El Thundering Herd Que Os Estáis Creando
Si scheduleáis 50 cron jobs que todas pegan la misma query resource-heavy al mismo minuto (e.g., cada hora en :00), estáis creando un thundering herd dentro de la base de datos.
Cómo prevenirlo:
1. Staggered schedules: No pongáis todas las jobs en minuto :00. Distribuidlas entre :00, :02, :04, :06...
2. Advisory locks: Para jobs que no pueden correr concurrentemente:
```sql
-- Obtener lock antes de ejecutar operación pesada
SELECT pg_advisory_lock(123456789); -- lock ID único para esta job
-- Ejecutar trabajo pesado
PERFORM heavy_data_aggregation();
-- Liberar lock
SELECT pg_advisory_unlock(123456789);
```
3. Condition-based triggers: Solo ejecutar cuando hay datos nuevos, no en timer fijo:
```sql
-- Job que detecta si hay trabajo pendiente
CREATE OR REPLACE FUNCTION job_conditional_aggregation()
RETURNS void AS $$
DECLARE
pending_count INTEGER;
BEGIN
SELECT COUNT(*) INTO pending_count
FROM pending_records
WHERE processed = false;
IF pending_count > 0 THEN
PERFORM process_pending_records();
RAISE NOTICE 'Procesados % registros pendientes', pending_count;
ELSE
RAISE NOTICE 'No hay registros pendientes. Job saltada.';
END IF;
END;
$$ LANGUAGE plpgsql;
```
Objections: Por Qué No Usar Edge Functions con Cron Triggers
"¿No puedo usar Edge Functions con cron triggers en vez de pg_cron?"
Edge Functions resuelven otro problema: corren fuera de la base de datos.
Si vuestra job es principalmente SQL — cleanup, agregación, refresh de materialized views — ejecutarla dentro de Postgres con pg_cron evita overhead de serialización/deserialización y mantiene consistencia transaccional.
El patrón híbrido es más potente que cualquiera de los dos solos:
pg_cron para trabajo SQL pesado
pg_net → Edge Function para side effects externos
Objections: ¿Y Redis Queue o RabbitMQ?
"¿No son más fiables los queue-based systems?"
Esto missing the point. pg_cron + pg_net no compite con sistemas de queue completos. Reemplaza el caso común donde equipos montan infraestructura de queue que no necesitan.
Para el 90% de pipelines recurrentes (ETL, cleanup, reporting, webhook calls), el ecosistema built-in de Postgres es suficiente.
El momento que necesitéis retry logic con dead-letter queues o exactly-once delivery semantics — sí, reached para una queue apropiada. Pero empezad simple.
Objections: Single Point of Failure
"Si Postgres cae, mi scheduler cae."
Esto es verdad pero misleading. Si Postgres está caído, vuestra aplicación ya está caída independientemente de dónde corra el scheduler.
La pregunta real es si el scheduler recupera correctamente cuando Postgres reinicia. pg_cron sí lo hace — almacena schedules en cron.job.
Si necesitáis el scheduler durante maintenance windows, un enfoque híbrido tiene sentido: pg_cron para ops normales, fallback externo para mantenimiento.
Conclusión
Vuestra base de datos Supabase es más inteligente de lo que le estáis dejando ser.
pg_cron + pg_net no es solo un scheduler. Es un orchestrator event-driven que puede transformar cómo vuestra aplicación reacciona a datos, eventos, y condiciones externas — sin una capa de aplicación separada, sin polling, sin servicios externos adicionales.
Key takeaways:
→ pg_cron corre como background worker dentro de Postgres — zero network hops, zero overhead
→ pg_net habilita HTTP requests asíncronos que no bloquean — triggers reactivos en vez de polling
→ El Patrón de Cascada Autónoma elimina dependencies de timing entre jobs
→ Instrumentad siempre con observabilidad desde el día uno
→ Los circuit breakers previenen cascadas de fallo cuando una job upstream falla
La próxima vez que reachéis para un servicio externo de scheduling, preguntáoslo: ¿no podría hacerlo mi base de datos?
Spoiler: probablemente sí.
Lee el artículo completo en brianmenagomez.com
Más sobre mis servicios en brianmenagomez.com
Herramientas: Conversor IAE CNAE · Gestorias cerca de ti · Calculadora IRPF

