Why Async Code Breaks Inside Celery Tasks (FastAPI + Celery)

PythonFastapiProgrammingceleryAsynchronous Programming
Why Async Code Breaks Inside Celery Tasks (FastAPI + Celery)

Why Async Code Breaks Inside Celery Tasks (FastAPI + Celery)

When you first pair FastAPI with Celery, everything feels smooth:

  • FastAPI handles asynchronous APIs
  • Celery handles heavy background jobs
  • Redis or RabbitMQ handles queueing

Then one day, you write something like this inside a Celery task:

@app.task
def process_report(user_id):
    data = await fetch_user_data(user_id)  # ❌ Problem!

Boom.

You get an error like:

RuntimeError: This event loop is already running

Or worse, the task hangs indefinitely.

So why does this happen, and what’s the clean way to solve it?


Why Async Code Fails Inside Celery Tasks

Celery workers are not asynchronous by default.

They execute tasks in separate worker processes and do not natively understand Python's async/await model.

When a Celery task tries to run:

await send_email()
await fetch_data()
await some_async_call()

Celery doesn't know how to manage the coroutine or event loop properly.

This often leads to:

  • RuntimeError: This event loop is already running
  • Event loop conflicts
  • Hanging tasks
  • Unpredictable behavior in production

Some developers attempt to work around this using:

asyncio.run(fetch_user_data(user_id))

While this may appear to work initially, it creates a new event loop for every task execution, which can become inefficient and problematic under load.


The Better Pattern: Let FastAPI Handle Async Work

Instead of executing asynchronous code directly inside Celery, delegate the async work back to FastAPI.

Principle

  • Celery remains responsible for orchestration and background scheduling.
  • FastAPI remains responsible for asynchronous operations.

The Celery worker simply triggers an internal FastAPI endpoint that executes the async logic safely.


Architecture

Client
   │
   ▼
FastAPI
   │
   ▼
Celery Task
   │
   ▼
Internal FastAPI Endpoint
   │
   ▼
Async Function

Flow:

Celery Task
      │
      ▼
HTTP Request
      │
      ▼
FastAPI Endpoint
      │
      ▼
await fetch_user_data()


Implementation

1. Internal FastAPI Endpoint

from fastapi import APIRouter
router = APIRouter()

@router.post("/internal/fetch-user")
async def fetch_user_endpoint(payload: dict):
    user_id = payload["user_id"]
    data= await fetch_user_data(user_id)
    return data

2. Synchronous Trigger Function

import requests

def trigger_user_processing(user_id):
    url = "http://localhost:8000/internal/fetch-user"

    response = requests.post(
        url,
        json={"user_id": user_id}
    )

    return response.json()

3. Celery Task

@app.task
def process_data(metadata):
    user_id = metadata.get("user_id")
    data = trigger_user_processing(user_id)

    # Additional processing
    # Update database
    # Send notifications
    # Generate reports

What Happened?

The Celery worker performs a normal synchronous HTTP request:

Celery → requests.post(...)

FastAPI receives the request and executes the async logic:

await fetch_user_data(user_id)

Since FastAPI is already running on an ASGI server such as Uvicorn, it has a proper event loop available.

No event loop conflicts occur.


Why This Works

Celery Stays Synchronous

No coroutine management is required inside the worker.

FastAPI Handles Async Operations

The event loop is managed by Uvicorn/ASGI, which is designed for asynchronous execution.

Clear Separation of Responsibilities

ComponentResponsibilityFastAPIAsync I/O operationsCeleryBackground orchestrationRedis/RabbitMQTask queueingWorkersLong-running processing

Easier to Scale

  • FastAPI instances can scale independently.
  • Celery workers can scale independently.
  • Async code remains centralized.

Considerations

While this pattern is simple and effective, it introduces an HTTP hop between Celery and FastAPI.

For high-throughput systems, alternative approaches may be preferable:

  • Using async-native task queues
  • Running async code through a dedicated service layer
  • Leveraging Celery's evolving async support (depending on version and requirements)

However, for many microservice architectures, an internal endpoint provides a clean and reliable solution.


Final Thoughts

If you're using FastAPI and Celery and encountering async/await issues:

  • Don't force async code directly into Celery tasks.
  • Don't rely heavily on asyncio.run() inside workers.
  • Let FastAPI own asynchronous operations.
  • Let Celery focus on orchestration and background execution.

A simple internal API call can eliminate event loop conflicts and keep your architecture predictable, maintainable, and production-friendly.