
The need for background processing is growing rapidly with AI use cases, especially when synchronous operations are untenable. AI document processing, image and video generation, deep research agents, and complex automations all share a common requirement: users submit requests and return later for results while orchestrations run on the backend, isolated from user-facing threads. The ideal architecture separates concerns into distinct microservices: one service handles user requests, another processes jobs. Connect them via Kafka, write metadata to a database, and scale independently. This setup works beautifully - if you have the infrastructure budget. The practical approach for indie hackers and MVPs is simpler: a single FastAPI service and a Redis instance. It's cost effective and sufficient for early-stage applications, but there's a critical gotcha: thread blocking.
Here is a typical flow for background job processing:
Here is the gotcha. When Redis handles a message, it uses the main thread by default. Without special handling, you get main thread blocking and an unresponsive API.
# ❌ This blocks your main thread
def message_handler(message):
# Process document batch - takes 10+ minutes
process_batch(message["data"])
redis_client.subscribe(channel, message_handler)
# When setting up the subscription on FastAPI
@app.on_event("startup")
async def start_app():
# This subscription blocks the main thread during message processing
redis.subscribe(RedisChannels.DOCUMENT_INGESTION, ingestion.process_batch)
# API becomes unresponsive during batch processingThe issue is that Redis processes messages synchronously on the thread that established the subscription and in a FastAPI application, this is typically your main event loop thread, causing complete application blocking. My solution combines Python's `ThreadPoolExecutor` with isolated async execution contexts to achieve non-blocking message processing. This architecture bridges the gap between async/await paradigm and the need for CPU bound parallelism.
The key insight is that Python's asyncio provides concurrency, but THreadPoolExecutor provides parallelism. By combining both, we have:
This' how the components work together:

This pattern works for the following reasons: