---
title: "Cloud Run Jobs for Heavy Celery Tasks"
description: "Moving compute-heavy async tasks (ffmpeg, TTS, Whisper) from Celery workers to Google Cloud Run Jobs"
tags: [architecture, gcp, cloud-run, celery, video-accessibility, workers]
created: 2026-04-29
updated: 2026-04-29
---

# Cloud Run Jobs for Heavy Celery Tasks

## Key Takeaways

- Cloud Run **Jobs** (not Services) are for finite tasks — pay only for execution time, no idle cost
- Heavy workers (`ffmpeg-worker`, `tts-worker`, `whisper-worker`) set to `replicas: 0` on environments that delegate to Cloud Run
- `USE_CELERY_FALLBACK=true` keeps `.delay()` working locally without Cloud Run
- `--redeploy` flag in deploy script = git pull + restart only (no rebuild) — critical for hotfixes
- Task chain (ingest→translate→render) must be dispatched from within the Cloud Run Job process itself — Celery callbacks don't work across the boundary

## Architecture: Celery → Cloud Run Jobs Migration

The Video Accessibility project (and similar heavy-AI pipelines) offloads:

| Task | From | To |
|------|------|----|
| `ingest_and_ai` | Celery `ai-queue` worker | Cloud Run Job |
| `translate_and_synthesize` | Celery `ai-queue` worker | Cloud Run Job |
| `render_accessible_video` | Celery `ai-queue` worker | Cloud Run Job |

Lightweight tasks (notifications, DB writes) stay on Celery `default` queue — see [[wiki/tech-patterns/redis-celery-worker-queue|redis-celery-worker-queue]].

## Environment-Specific Docker Compose

`docker-compose.optical-dev.yml` exists specifically because optical-dev has 2 CPUs and the prod compose had `cpus: '4.0'`:

```yaml
# docker-compose.optical-dev.yml
services:
  ffmpeg-worker:
    deploy:
      replicas: 0        # delegated to Cloud Run Jobs
      resources:
        limits:
          cpus: '1.0'    # optical-dev only has 2 CPUs

  tts-worker:
    deploy:
      replicas: 0
      resources:
        limits:
          cpus: '1.0'
```

See [[wiki/concepts/docker-compose-cpu-limits-env|docker-compose-cpu-limits-env]] for the broader pattern.

## Cloud Run Job Env Vars

Use `^|^` as delimiter when values contain `=` (JWT secrets, base64 tokens):

```bash
gcloud run jobs update va-worker \
  --set-env-vars "^|^KEY1=value1|KEY2=jwt=with=equals|KEY3=value3"
```

## Local Development Fallback

```env
USE_CELERY_FALLBACK=true
```

When this env var is set, `.delay()` calls route to local Celery workers instead of dispatching Cloud Run Jobs. Keeps local dev working without GCP credentials.

## Deploy Script Flags

```bash
./deploy.sh             # full rebuild + deploy
./deploy.sh --redeploy  # git pull + restart only (no docker build)
```

`--redeploy` is critical for hotfixes — build on GCP free-tier takes 10–15 min queued + ~10 min build. Use it whenever the code change doesn't require image rebuild.

## Cloud Build Image Reference

```
europe-west1-docker.pkg.dev/{project}/nexus/va-worker:latest
```

## Task Chaining Constraint

When tasks run as Cloud Run Jobs, Celery callback chains don't work:

```python
# BROKEN: translate_and_synthesize won't fire after ingest completes
ingest_and_ai.delay(video_id).then(translate_and_synthesize.s())

# CORRECT: chain from within the Cloud Run Job process
def run_ingest_job(video_id):
    result = ingest_and_ai(video_id)
    if result.ok:
        dispatch_cloud_run_job("translate_and_synthesize", video_id)
```

Alternative: use Cloud Workflows to orchestrate multi-step job chains.

## Related

- [[wiki/tech-patterns/redis-celery-worker-queue|redis-celery-worker-queue]] — Celery queue setup and AI queue separation
- [[wiki/concepts/docker-compose-cpu-limits-env|docker-compose-cpu-limits-env]] — why per-environment compose files are needed
- [[wiki/architecture/optical-dev-server-deploy|optical-dev-server-deploy]] — optical-dev server constraints

## Sources

- [[daily/2026-04-29.md]] — sessions 21:52 and 20:29 (Video Accessibility Cloud Run migration)
