Django
async programming
AppConfig
Python
web development

Django async with AppConfig.ready sync/async

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Django's AppConfig.ready() method runs during application startup to perform initialization tasks like connecting signals, running checks, or populating caches. This method is synchronous — it runs before the ASGI/WSGI server starts and before any event loop is available. You cannot use await inside ready() because there is no running event loop at that point. To run async code during startup, you must use asyncio.run(), sync_to_async, or defer the async work to the first request. Django 4.1+ provides better async support, but ready() itself remains synchronous.

The Problem

python
1# myapp/apps.py — THIS DOES NOT WORK
2from django.apps import AppConfig
3
4class MyAppConfig(AppConfig):
5    name = 'myapp'
6
7    async def ready(self):  # Django does not call this as async
8        await self.warm_cache()
9
10    async def warm_cache(self):
11        # This never executes correctly
12        async with aiohttp.ClientSession() as session:
13            response = await session.get('https://api.example.com/config')
14            data = await response.json()

Django calls ready() synchronously during django.setup(). Defining it as async def either causes it to return a coroutine object (never awaited) or raises SynchronousOnlyOperation.

Solution 1: asyncio.run() for One-Shot Async Work

python
1import asyncio
2from django.apps import AppConfig
3
4class MyAppConfig(AppConfig):
5    name = 'myapp'
6
7    def ready(self):
8        # Run async code synchronously during startup
9        asyncio.run(self.warm_cache())
10
11    async def warm_cache(self):
12        import aiohttp
13        async with aiohttp.ClientSession() as session:
14            response = await session.get('https://api.example.com/config')
15            data = await response.json()
16            # Store in module-level cache
17            from myapp import cache
18            cache.CONFIG_DATA = data

asyncio.run() creates a new event loop, runs the coroutine, and closes the loop. This works because no event loop is running during ready().

Solution 2: sync_to_async with Django's Async Utilities

python
1from django.apps import AppConfig
2from asgiref.sync import async_to_sync
3
4class MyAppConfig(AppConfig):
5    name = 'myapp'
6
7    def ready(self):
8        # Convert async function to sync for calling in ready()
9        async_to_sync(self.initialize_async_components)()
10
11    async def initialize_async_components(self):
12        from myapp.services import AsyncCacheService
13        service = AsyncCacheService()
14        await service.connect()
15        await service.populate_initial_data()

Solution 3: Defer Async Work to First Request

If the async initialization can wait, use a middleware or signal to run it on the first request.

python
1# myapp/middleware.py
2import asyncio
3from django.utils.decorators import sync_and_async_middleware
4
5_initialized = False
6
7@sync_and_async_middleware
8def async_init_middleware(get_response):
9    async def middleware(request):
10        global _initialized
11        if not _initialized:
12            await warm_cache()
13            _initialized = True
14        response = await get_response(request)
15        return response
16    return middleware
17
18async def warm_cache():
19    import aiohttp
20    async with aiohttp.ClientSession() as session:
21        response = await session.get('https://api.example.com/config')
22        # Cache the response

Solution 4: Signal Connection in ready() (Common Use Case)

The most common use of ready() is connecting signals, which is synchronous and works without any async workarounds.

python
1from django.apps import AppConfig
2
3class MyAppConfig(AppConfig):
4    default_auto_field = 'django.db.models.BigAutoField'
5    name = 'myapp'
6
7    def ready(self):
8        # Import signals — this is the standard pattern
9        import myapp.signals  # noqa: F401
10
11        # Or connect signals directly
12        from django.db.models.signals import post_save
13        from myapp.handlers import handle_user_saved
14        from django.contrib.auth.models import User
15
16        post_save.connect(handle_user_saved, sender=User)

Solution 5: Background Task for Async Initialization

python
1import threading
2import asyncio
3from django.apps import AppConfig
4
5class MyAppConfig(AppConfig):
6    name = 'myapp'
7
8    def ready(self):
9        # Start async initialization in a background thread
10        # Does not block startup
11        thread = threading.Thread(target=self._run_async_init, daemon=True)
12        thread.start()
13
14    def _run_async_init(self):
15        loop = asyncio.new_event_loop()
16        asyncio.set_event_loop(loop)
17        try:
18            loop.run_until_complete(self._async_init())
19        finally:
20            loop.close()
21
22    async def _async_init(self):
23        import aiohttp
24        async with aiohttp.ClientSession() as session:
25            response = await session.get('https://api.example.com/config')
26            data = await response.json()
27            # Store cached data

Common Pitfalls

  • Defining ready() as async def: Django calls ready() synchronously. Declaring it async def returns a coroutine object that is never awaited. The initialization code silently does not execute, and Python may emit a "coroutine was never awaited" warning.
  • Calling asyncio.run() when an event loop is already running: If Django is running under an ASGI server (like Uvicorn/Daphne), an event loop may already exist during startup in some configurations. asyncio.run() raises RuntimeError: This event loop is already running. Use async_to_sync() from asgiref instead, which handles this case.
  • Database operations in ready() before migrations: ready() runs during django.setup(), which happens before migrations in management commands like migrate. Performing database queries in ready() can fail if the table does not exist yet. Guard with try/except or check connection.introspection.table_names().
  • Importing models at module level in apps.py: Importing models at the top of apps.py causes AppRegistryNotReady because the app registry is still being populated. Import models inside ready() or inside signal handler functions.
  • Running ready() multiple times: Django can call ready() more than once (e.g., during test setup). Use a module-level flag (_initialized = False) to ensure one-time initialization logic runs only once.

Summary

  • AppConfig.ready() is synchronous — you cannot await inside it directly
  • Use asyncio.run() for one-shot async initialization during startup
  • Use async_to_sync() from asgiref when running under ASGI servers
  • Defer non-critical async work to a background thread or first-request middleware
  • The most common ready() use case (connecting signals) is synchronous and needs no async handling

Course illustration
Course illustration

All Rights Reserved.