Django & FastAPI
WSGI vs ASGI, and why WSGI cannot serve WebSockets
WSGI (PEP 3333) is one synchronous callable, application(environ, start_response): it takes a single request and returns one iterable of bytes. That shape leaves no room for a long-lived connection or several incoming events on one connection, so it cannot carry WebSocket frames or HTTP long-poll. ASGI is the async successor, async def application(scope, receive, send), where receive and send are async callables that stream event messages in both directions. Django ships both entry points (wsgi.py and asgi.py); FastAPI and Starlette speak ASGI directly.
Wrong "WSGI handles WebSockets once the view is async def." The limit is the interface, not the view. Right WSGI is a single request-in, response-out contract with no channel for socket frames, so you serve the app over ASGI instead.
The Django ORM N+1 trap
A QuerySet is lazy: Book.objects.all() builds SQL but hits the database only when you iterate it, call list() or len(), or slice it with a step. The common bug is a loop that reads book.author.name, firing one query per row on top of the initial list query. That is N+1.
Match the fix to the relation:
select_related("author")for ForeignKey and OneToOne: one query with a SQL JOIN, and the related row travels with the parent.prefetch_related("tags")for ManyToMany and reverse ForeignKey: a second query whose rows are matched to the parents in Python.
book.author is cached after the first access, but book.tags.all() re-runs its query on every call unless the relation was prefetched.
def vs async def in FastAPI
Declare a path operation def and FastAPI runs it in an external threadpool, so a blocking client or a synchronous database driver does not stall the server. Declare it async def and it runs on the event loop, where the only safe blocking primitive is await on non-blocking I/O.
Wrong "Making the view async def makes it faster." Right async def helps only when you await non-blocking I/O; a blocking call (time.sleep, requests.get, a sync driver, a CPU loop) inside async def freezes the single event-loop thread and stalls every other request on that worker. If the libraries block and you are unsure, use def.
How a blocking call freezes an async worker
An async def view runs on one event-loop thread. A blocking call inside it (time.sleep, requests.get, a synchronous database driver, a tight CPU loop) holds that thread and stalls every concurrent request the worker is handling. Offload it with await run_in_threadpool(fn), anyio.to_thread.run_sync, or asyncio.to_thread for I/O, and a process pool for CPU-bound work.
The threadpool has a ceiling. Starlette runs def endpoints and def dependencies on an AnyIO threadpool whose default size is 40 threads, shared across all of them. Send more than about 40 slow synchronous calls at once and the rest queue behind the limiter while the loop sits idle, so throughput drops. Raise the limit through anyio.to_thread.current_default_thread_limiter().total_tokens when the sync work is I/O-bound.
Wrong "def endpoints block the event loop, so make everything async def." Right def path operations and dependencies run in that threadpool and never touch the loop; the loop freezes only when blocking code sits inside an async def.
The Django ORM across the async boundary
Calling the synchronous ORM inside an async def view raises SynchronousOnlyOperation, because the ORM is async-unsafe. Use the native async methods (aget, acreate, async for) or wrap the call in sync_to_async. That wrapper defaults to thread_sensitive=True, which serializes the wrapped calls onto one shared thread because the ORM assumes a single thread, so it guards correctness and does not add parallelism.
transaction.atomic and the broken transaction
Django runs in autocommit by default: each query commits on its own unless a transaction is open. transaction.atomic(), as a decorator or a context manager, commits on clean exit and rolls back on any exception; nested atomic() blocks use savepoints, so an inner block rolls back only to its savepoint.
Wrong "Catch the database error inside the atomic block and keep going." Once a query has failed the transaction is broken, and every later query raises TransactionManagementError until the block unwinds. Right put the try/except around an inner atomic() block, so the failure rolls back that savepoint before you handle it.
Gunicorn prefork vs Uvicorn ASGI workers
Gunicorn uses a pre-fork model: an arbiter forks N worker processes and restarts any that crash. The default worker_class is sync, which serves one request at a time per worker, keeps no connection alive, and expects a buffering reverse proxy in front. Concurrency comes from adding workers, gthread, or an async worker class. A rough starting point is (2 * CPU) + 1, usually 4 to 12, and each worker is a full process that copies app memory.
For ASGI apps (FastAPI, or Django through asgi.py) run Uvicorn workers. The current combination is gunicorn app:app -k uvicorn_worker.UvicornWorker from the uvicorn-worker package; the older -k uvicorn.workers.UvicornWorker has been deprecated since Uvicorn 0.30.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.