Un retry puede salvar una task que falló. También puede repetir una escritura que ocurrió antes de que el proceso muriera. Si la pipeline crea un archivo, inserta un registro o cobra una operación, “intentarlo otra vez” no es una política de idempotencia.

En Cloud Run Jobs, la protección debe vivir en tu código y en almacenamiento durable. Dale a la task una identidad estable, persiste el progreso fuera del contenedor y haz que la salida acepte la misma clave una sola vez. El retry puede repetir la ejecución sin convertir un fallo parcial en dos resultados.

Este artículo usa TypeScript ilustrativo y un Job por lotes. Para elegir entre Service, Job y worker pool, consulta la comparación de recursos de Cloud Run para agentes de larga duración. Aquí el problema es más concreto: cómo recupera su trabajo una task reiniciada sin duplicar el efecto.

Diagrama muestra una task de Cloud Run que se reinicia mediante retry, checkpoint, idempotencia y verificación.

Regla práctica

  • Retry decide cuántas veces puede intentarlo una task.
  • Un checkpoint decide desde dónde continúa un intento.
  • La idempotencia decide si repetir una entrada crea otro efecto.
  • La verificación demuestra que la salida esperada existe una vez, no solo que el proceso terminó.

¿Por qué un retry no hace idempotente una task?

En 2026, la documentación de retries y checkpoints de Cloud Run Jobs dice que cada task puede fallar, reiniciarse e intentarlo de nuevo. El valor predeterminado documentado es de hasta 3 retries. Eso mejora la posibilidad de recuperarse de fallos transitorios, pero no impide que dos intentos lleguen a la misma operación externa.

Imagina una task que calcula un resultado, escribe result.json y muere antes de marcar el estado como terminado. El siguiente intento no sabe si debe recalcular, sustituir el archivo o saltarse la etapa. Si la salida es un insert sin clave única, puede crear una segunda fila. Si es un correo o un cobro, reducir max-retries no deshace el efecto.

La primera separación es sencilla: retry es una política de recuperación de la plataforma; idempotencia es una propiedad de la operación. Puedes configurar cero retries y duplicar un resultado al ejecutar el Job manualmente. También puedes configurar tres retries y evitar duplicados si cada efecto usa la misma clave y una escritura condicional.

¿Qué debe sobrevivir a un reinicio?

En 2026, la guía de Google Cloud sobre retries y checkpoints recomienda hacer idempotentes los Jobs y usar checkpoints persistentes para que una task reiniciada no repita todo su trabajo. La memoria del proceso no es un checkpoint. Desaparece cuando la instancia se detiene.

Para cada unidad de trabajo, persiste cuatro cosas:

  1. Identidad: nombre del Job, índice de task y una clave de negocio que no cambie entre intentos.
  2. Estado: queued, running, checkpointed, done o failed.
  3. Progreso: última etapa terminada y ubicación de su artefacto parcial.
  4. Salida: registro u objeto dirigido por la misma clave de idempotencia.

La guía de ejecución durable para agentes y pipelines explica la frontera más amplia entre checkpoints, colas y workflows. Para un Cloud Run Job no necesitas empezar por un orquestador. Necesitas que un nuevo intento lea el estado durable y distinga “la etapa no empezó” de “la salida ya fue confirmada”.

No guardes un único checkpoint después de toda la pipeline. Guarda cada etapa que produzca un artefacto útil. Un reintento puede saltarse la decodificación que ya terminó y continuar con la transformación. El checkpoint reduce trabajo repetido. La clave idempotente protege el efecto cuando el proceso muere entre la escritura y la confirmación.

Diagrama muestra una pipeline con checkpoint persistente y una clave de idempotencia que bloquea una salida duplicada.

¿Cómo escribir una etapa idempotente en TypeScript?

Cloud Run expone el índice de la task mediante CLOUD_RUN_TASK_INDEX, pero la identidad de la salida sigue siendo una decisión de la aplicación. La documentación de creación de Jobs describe ese índice y el número total de tasks. Úsalo cuando la partición del lote forme parte del contrato. Para una entidad de negocio, incluye también un ID estable del elemento.

El siguiente ejemplo es ilustrativo. putIfAbsent representa una operación atómica en tu base de datos o almacén de objetos. Sin esa condición, dos intentos concurrentes pueden observar “falta” y escribir dos salidas.

type Progress = {
  key: string;
  status: "running" | "checkpointed" | "done";
  artifactKey?: string;
  leaseUntil: number;
};

interface ProgressStore {
  get(key: string): Promise<Progress | null>;
  claim(key: string, leaseUntil: number): Promise<"claimed" | "busy" | "done">;
  put(value: Progress): Promise<void>;
  putIfAbsent(key: string, value: { artifactKey: string }): Promise<boolean>;
}

function outputKey(jobName: string, taskIndex: string, itemId: string) {
  return `jobs/${jobName}/tasks/${taskIndex}/items/${itemId}/result.json`;
}

async function runItem(input: {
  jobName: string;
  taskIndex: string;
  itemId: string;
  store: ProgressStore;
  buildArtifact: () => Promise<string>;
}) {
  const key = outputKey(input.jobName, input.taskIndex, input.itemId);
  const existing = await input.store.get(key);

  if (existing?.status === "done") return existing.artifactKey;
  const claim = await input.store.claim(key, Date.now() + 5 * 60_000);
  if (claim === "done") return existing?.artifactKey;
  if (claim === "busy") {
    throw new Error("item is already leased");
  }

  const artifactKey = existing?.artifactKey ?? await input.buildArtifact();
  await input.store.put({ key, status: "checkpointed", artifactKey, leaseUntil: 0 });
  await input.store.putIfAbsent(key, { artifactKey });
  await input.store.put({ key, status: "done", artifactKey, leaseUntil: 0 });

  return artifactKey;
}

El contrato tiene dos decisiones que no pueden quedar implícitas. claim debe ser atómico y el lease debe expirar porque un proceso puede morir sin liberar running. La escritura final debe ser condicional o usar una clave única. En producción, haz atómicas también las transiciones de estado. El ejemplo no elige Firestore, Cloud Storage o PostgreSQL; muestra las garantías que debe ofrecer cualquier adaptador.

Para efectos externos, como una tool call que crea un recurso, envía la misma clave de idempotencia al destino cuando la soporte. Si no la soporta, coloca la operación detrás de una tabla de intenciones o de un paso de aprobación. La validación de tool calls en TypeScript ayuda a controlar el schema, pero no impide repetir un efecto válido.

¿Cómo configurar retries y timeout en Cloud Run Job?

En 2026, la documentación de creación de Jobs indica un timeout predeterminado de 10 minutos por task y un número de retries configurable. La documentación del timeout de tasks permite subirlo hasta 168 horas para tasks sin GPU. Esos límites se aplican a cada intento. No prometen que una ejecución permanezca activa sin interrupciones.

Un comando inicial podría ser:

gcloud run jobs create process-items \
  --image=REGION-docker.pkg.dev/PROJECT/REPOSITORY/IMAGE:TAG \
  --tasks=1 \
  --max-retries=3 \
  --task-timeout=20m \
  --region=REGION

Este comando es un ejemplo de configuración, no un despliegue listo. Confirma región, imagen, cuenta de servicio y límites del proyecto. Elige suficientes retries para fallos transitorios, pero no trates el número como solución de un efecto no idempotente.

Si el lote puede dividirse en tasks independientes, usa la identidad de la task para crear particiones repetibles. El código debe asignar el mismo elemento a la misma partición en cada intento. Si la lista de entrada cambia durante la ejecución, registra la versión o snapshot usado. De lo contrario, una misma task puede procesar elementos distintos en un retry y hacer confuso el historial.

¿Cómo separar fallos retryable y terminales?

Google Cloud documenta retries para fallos que pueden ser transitorios, pero la aplicación aún debe decidir cuándo sirve otro intento. Una respuesta 429, un timeout de red o un error 5xx puede merecer retry. Un archivo ausente, un schema inválido o un permiso denegado suele necesitar corrección o cuarentena.

Usa una clasificación explícita:

  1. Registra elemento, intento, etapa y motivo antes de lanzar el error.
  2. Lanza solo fallos que puedan cambiar sin modificar input o configuración.
  3. Marca errores terminales como failed con contexto suficiente para repararlos.
  4. Envía retries agotados a una dead-letter queue o a un informe.
  5. Haz que un watchdog busque leases vencidos y estados atascados en running.

No confundas “la función devolvió un error” con “el efecto no ocurrió”. El proceso puede escribir en la base de datos y morir antes de la última línea del log. La recuperación debe consultar el estado durable antes de ejecutar de nuevo, no buscar solo la última línea de log.

¿Cómo demostrar que un reintento no duplicó la salida?

Después de configurar el Job, prueba una caída en varios puntos: antes del checkpoint, después del artefacto y antes de la confirmación final. Un Job es seguro para reintentar solo cuando el segundo intento encuentra la misma identidad y produce el mismo resultado observable.

Una verificación mínima puede verse así:

it("reutiliza la salida cuando la misma task se ejecuta dos veces", async () => {
  const store = makeInMemoryStore();
  let builds = 0;

  const input = {
    jobName: "process-items",
    taskIndex: "0",
    itemId: "item-7",
    store,
    buildArtifact: async () => {
      builds += 1;
      return "artifacts/item-7.json";
    },
  };

  await runItem(input);
  await runItem(input);

  expect(builds).toBe(1);
  expect(await store.countOutputs("item-7")).toBe(1);
});

La prueba es una especificación de comportamiento. makeInMemoryStore y countOutputs deben existir en la suite real. Añade una prueba que falle después de buildArtifact y otra que ejecute dos workers contra el mismo lease. El resultado importante no es una string idéntica. Es la cantidad de salidas, la clave persistida y el estado final.

La documentación de Jobs dice que, cuando una task supera su límite de retries, la ejecución termina como fallida después de que Cloud Run haya probado las tasks. Inspecciona la ejecución y los logs, identifica la task fallida y compara la cantidad de artefactos con el snapshot de entrada. Un Job verde sin esa comprobación aún puede haber procesado el input equivocado.

Errores habituales y límites

El primer error es usar --max-retries=0 como sustituto de idempotencia. Desactiva retries automáticos. No detiene ejecuciones manuales, input repetido ni dos workers compitiendo por una clave.

El segundo es guardar solo el archivo final. Si termina una etapa cara y falla la siguiente, el reintento no sabe si el archivo es válido, qué versión lo creó ni qué input representa. Incluye versión del input, nombre de etapa y timestamp en el registro de progreso.

El tercero es hacer durable el estado, pero no el efecto. Un registro done escrito antes de la llamada externa puede ocultar un fallo. Registra la intención, ejecuta con una clave de idempotencia cuando puedas y confirma el estado solo después de comprobar el resultado.

El cuarto es meter todas las etapas en una task larga. La guía para probar agentes con mock y API real ayuda a comprobar que un reintento llama a la dependencia externa en el momento correcto. Para una pipeline de varias etapas, los checkpoints direccionables permiten repetir solo lo necesario. No son una transacción distribuida: un crash aún puede ocurrir entre dos sistemas, por lo que la recuperación debe ser segura en ese intervalo.

Preguntas frecuentes sobre retries en Cloud Run Jobs

¿Cuántos retries hace un Cloud Run Job por defecto?

La documentación de creación de Jobs indica hasta 3 retries por task por defecto, y el valor puede configurarse entre 0 y 10. El retry pertenece a la task. No es una garantía de que un efecto externo ocurra una sola vez. Usa el ajuste para fallos transitorios e implementa la idempotencia por separado.

¿Un checkpoint elimina la necesidad de idempotencia?

No. Un checkpoint reduce trabajo repetido e indica desde dónde continuar, pero el proceso puede morir después de escribir y antes de guardar el checkpoint. La guía de retries recomienda ambas prácticas. Usa esos dos controles: progreso persistente para recuperar la etapa y una clave única o escritura condicional para proteger la salida.

¿Cuál es el timeout predeterminado de una task?

En 2026, el timeout predeterminado documentado es de 10 minutos. Puede reducirse o subir hasta 168 horas para tasks sin GPU, mientras las tasks con GPU tienen un límite de 1 hora. El timeout se aplica a cada intento. Divide el trabajo o guarda checkpoints cuando una etapa pueda superarlo.

¿max-retries=0 evita todos los duplicados?

No. Con 0 retries automáticos, impide nuevos intentos después de un fallo, pero no ejecuciones manuales, input repetido ni dos workers compitiendo por una clave. La defensa es una identidad determinista, un lease con expiración, una escritura condicional y una comprobación que cuente la salida confirmada.

Fuentes consultadas