All Articles

How to Build Idempotent Cloud Tasks Handlers in Python

· 8 min read · Humza Tareen
GCP Cloud Tasks Python FastAPI Distributed Systems

The Problem

Google Cloud Tasks guarantees at-least-once delivery. That means your handler WILL be called multiple times for the same task. If you're not handling this, you have bugs.

# BAD: This creates duplicates on retry
@app.post("/tasks/score")
async def handle_score_task(request: ScoreRequest):
    result = ScoringResult(
        id=uuid4(),  # New ID every time!
        evaluation_id=request.evaluation_id,
        score=compute_score(request),
    )
    db.add(result)
    db.commit()

Cloud Tasks retries if your handler returns 5xx, takes longer than the timeout, or the network hiccups. Every retry creates a new row with a new UUID.

The Fix: 3-Step Idempotency Pattern

Step 1: Deterministic IDs

import hashlib

def compute_task_id(evaluation_id: str, model: str, turn: int) -> str:
    payload = f"{evaluation_id}:{model}:{turn}"
    return hashlib.sha256(payload.encode()).hexdigest()[:32]

Step 2: Guarded Upserts

INSERT INTO scoring_results (id, evaluation_id, score, model, turn)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (id) DO NOTHING
RETURNING id;

Step 3: Always Return 200

If your handler detects a duplicate and returns 409 (Conflict), Cloud Tasks will retry it. Forever. Always return 200 for idempotent operations.

Handling Multi-Step Tasks

Each step gets its own deterministic ID derived from the parent task ID. If the handler crashes after step 2, the retry skips steps 1 and 2 (already exist) and only executes step 3.

Cloud Run Configuration

# Don't overwhelm the DB
containerConcurrency: 10  # Not 80
timeoutSeconds: 3600      # Match your longest task

We reduced concurrency from 80 to 10 per instance. Each handler holds a database connection. 80 concurrent handlers × 10 instances = 800 connections. Our Cloud SQL instance can't handle that.

Key Takeaways

ProblemSolution
Random UUIDsDeterministic IDs from payload
INSERTINSERT ... ON CONFLICT DO NOTHING
Return 409 on duplicateReturn 200 always
No loggingCorrelation IDs from Cloud Tasks headers
High concurrencyCap containerConcurrency (10, not 80)

This pattern handles our entire evaluation pipeline — thousands of tasks per day, zero duplicates, zero data corruption.