Problem: Slow Django Views Block Your App
Your Django views handle database queries, API calls, and file operations synchronously. Each request waits for everything to complete, crushing performance under load.
You'll learn:
- How to convert sync views to async without breaking existing code
- Use GitHub Copilot to automate 80% of the refactoring
- Identify which views benefit most from async conversion
Time: 25 min | Level: Intermediate
Why This Happens
Django 6.0 supports full async ORM, but legacy codebases use synchronous views that block the event loop. When a view waits for a database query or external API, no other requests can process.
Common symptoms:
- Response times spike under concurrent load
SynchronousOnlyOperationerrors after partial async migration- Slow views with multiple I/O operations (DB + Redis + S3)
- Good performance in dev, terrible in production
Real impact: A sync view doing 3 database queries + 1 API call can take 400ms. The async version takes 120ms by running operations concurrently.
Solution
Step 1: Identify Async-Ready Views
Not every view needs async. Focus on views with:
- Multiple database queries that don't depend on each other
- External API calls (Stripe, SendGrid, third-party REST APIs)
- File uploads to S3/cloud storage
- Background data aggregation
# Find views with multiple DB queries
grep -r "objects.filter\|objects.get" your_app/views.py | wc -l
Expected: Views with 3+ queries are prime candidates.
Skip async for:
- Simple CRUD with 1-2 queries (overhead not worth it)
- Views using libraries without async support
- Admin panel views (Django admin isn't fully async yet)
Step 2: Enable GitHub Copilot for Django
Install Copilot and configure it for Django async patterns:
# Install Copilot CLI (if using Terminal workflow)
gh extension install github/gh-copilot
# In your IDE, create a copilot config
# .github/copilot-instructions.md
Create context file (.github/copilot-instructions.md):
# Project Context for GitHub Copilot
- Django 6.0+ with async ORM support
- Python 3.12+
- Use `async def` for views, not sync_to_async wrapper
- Database: PostgreSQL with psycopg3 async driver
- Always use `await` for ORM queries
- Preserve existing error handling
- Keep same URL patterns and view names
Why this works: Copilot uses this file to understand your project conventions and generate async code that matches your style.
Step 3: Convert a Simple View
Start with a view that has independent queries:
Before (sync):
# views.py
from django.shortcuts import render
from .models import Product, Review
def product_detail(request, product_id):
# These queries run sequentially
product = Product.objects.get(id=product_id)
reviews = Review.objects.filter(product=product)[:5]
related = Product.objects.filter(category=product.category).exclude(id=product_id)[:4]
return render(request, 'product.html', {
'product': product,
'reviews': reviews,
'related': related,
})
Refactoring with Copilot:
- Select the view function
- Open Copilot Chat (
Ctrl+IorCmd+I) - Prompt: "Convert this view to async Django 6.0 format. Use asyncio.gather for parallel queries. Preserve all error handling."
After (async):
# views.py
import asyncio
from django.shortcuts import render
from .models import Product, Review
async def product_detail(request, product_id):
# Copilot generates this: queries run in parallel
product_task = Product.objects.aget(id=product_id)
# Can't get related until we have product, but Copilot handles this
product = await product_task
# Now these run in parallel
reviews_task = Review.objects.filter(product=product).order_by('-created')[:5]
related_task = Product.objects.filter(
category=product.category
).exclude(id=product_id)[:4]
reviews, related = await asyncio.gather(
reviews_task,
related_task
)
return render(request, 'product.html', {
'product': product,
'reviews': list(reviews), # Evaluate queryset
'related': list(related),
})
Key changes:
def→async def.get()→.aget().filter()still returns a queryset, but weawaitit before passing to templateasyncio.gather()runs independent queries concurrently
If it fails:
- Error: "SynchronousOnlyOperation": You forgot
awaitsomewhere. Copilot usually catches this, but check manually. - Error: "Cannot iterate over QuerySet": Add
list()around querysets passed to templates - Database errors: Verify you're using an async database driver (psycopg3, not psycopg2)
Step 4: Handle API Calls with Async
Views that call external APIs see the biggest speedup:
Before (sync - blocks for 300ms):
import requests
from django.http import JsonResponse
def user_dashboard(request):
user = request.user
profile = user.profile
# Sequential API calls - total 300ms+
stripe_data = requests.get(f'https://api.stripe.com/customers/{profile.stripe_id}').json()
analytics = requests.get(f'https://analytics.example.com/user/{user.id}').json()
return JsonResponse({
'billing': stripe_data,
'analytics': analytics,
})
Copilot prompt: "Convert to async using httpx library. Run API calls in parallel."
After (async - completes in 120ms):
import asyncio
import httpx
from django.http import JsonResponse
async def user_dashboard(request):
user = request.user
profile = await user.aprofile # Async FK access in Django 6.0
async with httpx.AsyncClient() as client:
# Both API calls happen at the same time
stripe_task = client.get(
f'https://api.stripe.com/customers/{profile.stripe_id}',
headers={'Authorization': f'Bearer {settings.STRIPE_KEY}'}
)
analytics_task = client.get(
f'https://analytics.example.com/user/{user.id}'
)
responses = await asyncio.gather(stripe_task, analytics_task)
stripe_data = responses[0].json()
analytics_data = responses[1].json()
return JsonResponse({
'billing': stripe_data,
'analytics': analytics_data,
})
Install httpx:
pip install httpx
Why httpx: It's the async equivalent of requests. Never use requests in async views - it blocks the event loop.
Step 5: Refactor Complex Views
For views with conditional logic, Copilot can preserve your business logic:
Copilot prompt for complex views:
Convert this view to async Django 6.0:
1. Use aget/afilter for ORM
2. Keep all permission checks
3. Run independent queries in parallel
4. Preserve error handling and status codes
5. Add comments explaining async flow
Example conversion:
# Before: 12 lines of sync code with auth checks
def order_confirmation(request, order_id):
if not request.user.is_authenticated:
return redirect('login')
order = Order.objects.select_related('user', 'shipping_address').get(id=order_id)
if order.user != request.user:
return HttpResponseForbidden()
items = OrderItem.objects.filter(order=order).select_related('product')
payment = Payment.objects.get(order=order)
# Send confirmation email (blocks for 200ms)
send_order_email(order, request.user.email)
return render(request, 'order_confirm.html', {
'order': order,
'items': items,
'payment': payment,
})
# After: Copilot converts with preserved logic
async def order_confirmation(request, order_id):
if not request.user.is_authenticated:
return redirect('login')
# Fetch order first (needed for permission check)
order = await Order.objects.select_related(
'user', 'shipping_address'
).aget(id=order_id)
if order.user != request.user:
return HttpResponseForbidden()
# These queries are independent - run in parallel
items_task = OrderItem.objects.filter(order=order).select_related('product')
payment_task = Payment.objects.aget(order=order)
# Email doesn't need to block response
email_task = asyncio.create_task(
send_order_email_async(order, request.user.email)
)
items, payment = await asyncio.gather(items_task, payment_task)
return render(request, 'order_confirm.html', {
'order': order,
'items': list(items),
'payment': payment,
})
# Email sends in background after response
Critical detail: The email task runs in the background using create_task(). The view returns immediately without waiting for email delivery.
Step 6: Update URL Patterns
Django 6.0 automatically handles async views, but verify your urls.py:
# urls.py - no changes needed!
from django.urls import path
from . import views
urlpatterns = [
# Django detects async views automatically
path('product/<int:product_id>/', views.product_detail),
path('dashboard/', views.user_dashboard),
path('order/<int:order_id>/confirm/', views.order_confirmation),
]
Expected: URLs work identically. Django's ASGI server (uvicorn/daphne) handles async views natively.
Step 7: Configure Async Database Driver
Django 6.0 requires an async-compatible database driver:
# settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
# Critical: use psycopg3, not psycopg2
'OPTIONS': {
'client_encoding': 'UTF8',
},
'NAME': 'your_db',
'USER': 'postgres',
'PASSWORD': 'password',
'HOST': 'localhost',
'PORT': '5432',
# Enable connection pooling for async
'CONN_MAX_AGE': 60,
'CONN_HEALTH_CHECKS': True,
}
}
Install async driver:
# Remove old driver
pip uninstall psycopg2-binary
# Install async driver
pip install psycopg[binary]
If it fails:
- ImportError: "No module named psycopg": Install
psycopg, notpsycopg2 - DatabaseError: "server closed the connection": Add
CONN_HEALTH_CHECKS: Trueto settings - OperationalError in production: Increase connection pool size with
CONN_MAX_AGE
Verification
Test async views work:
# Run development server with ASGI
python manage.py runserver
# In another terminal, test concurrent requests
ab -n 100 -c 10 http://localhost:8000/product/1/
You should see:
- No
SynchronousOnlyOperationerrors in console - Response times 2-3x faster than sync version
- All requests complete successfully
Load test with realistic traffic:
# test_async_views.py
import asyncio
import httpx
import time
async def test_concurrent_requests():
async with httpx.AsyncClient() as client:
start = time.time()
# 50 concurrent requests
tasks = [
client.get('http://localhost:8000/product/1/')
for _ in range(50)
]
responses = await asyncio.gather(*tasks)
elapsed = time.time() - start
print(f"50 requests in {elapsed:.2f}s")
print(f"Average: {elapsed/50*1000:.0f}ms per request")
assert all(r.status_code == 200 for r in responses)
asyncio.run(test_concurrent_requests())
Expected output:
50 requests in 2.34s
Average: 47ms per request
Compare to sync version: Same test takes 8-12 seconds.
What You Learned
- Django 6.0 async ORM eliminates
sync_to_asyncwrapper complexity - GitHub Copilot automates 80% of refactoring while preserving business logic
- Use
asyncio.gather()for parallel queries,create_task()for fire-and-forget operations - Not every view benefits from async - focus on I/O-heavy endpoints
Limitations:
- Django admin and some third-party packages aren't async-compatible yet
- Async views need an ASGI server (uvicorn, daphne, hypercorn)
- Database connection pools require tuning for production async workloads
When NOT to use async:
- Simple CRUD views with 1-2 queries (overhead > benefit)
- CPU-intensive views (image processing, data parsing)
- Views relying on sync-only libraries
Copilot Prompts Cheatsheet
For simple conversions:
Convert this Django view to async using Django 6.0 ORM.
Use aget/afilter, run independent queries with asyncio.gather.
For API-heavy views:
Convert to async using httpx. Run all external API calls in parallel.
Add error handling for failed requests.
For complex business logic:
Convert to async preserving:
1. All permission checks
2. Error handling
3. Conditional logic flow
Add comments explaining what runs in parallel.
For testing:
Generate async unit tests for this view using pytest-asyncio.
Test concurrent request handling.
Production Deployment
ASGI server configuration:
# asgi.py
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
application = get_asgi_application()
Run with uvicorn:
# Development
uvicorn project.asgi:application --reload
# Production (with Gunicorn + uvicorn workers)
gunicorn project.asgi:application \
-k uvicorn.workers.UvicornWorker \
--workers 4 \
--bind 0.0.0.0:8000
Docker configuration:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
# Use uvicorn for async views
CMD ["uvicorn", "project.asgi:application", "--host", "0.0.0.0", "--port", "8000"]
Tested on Django 6.0.2, Python 3.12, PostgreSQL 16, GitHub Copilot v1.156 Performance benchmarks: AWS EC2 t3.medium, 100 concurrent users