Узнайте, как Django управляет памятью: разница между startup и каждым запросом, типичные ошибки с глобальными переменными и правила безопасного кода.
Если вы перешли с PHP, вы привыкли, что каждый запрос обрабатывается с нуля. В Python всё иначе: веб-сервер (например, Gunicorn) запускает один или несколько рабочих процессов, которые живут постоянно и обрабатывают множество запросов. В этой статье разберём, как устроена память в Django, чтобы избежать утечек и неожиданного поведения.
Когда вы запускаете Django-сервер (встроенный dev-сервер или Gunicorn), есть два этапа:
wsgi.py, вызывается get_wsgi_application(), загружаются настройки, импортируются приложения из INSTALLED_APPS, вызываются методы ready() конфигов приложений, компилируются URL-паттерны.Django dev-сервер использует один рабочий процесс с потоками. Gunicorn запускает несколько рабочих процессов (отдельных копий приложения). Обычная формула: (2 × число_ядер_CPU) + 1 рабочих процессов.
Если у вас есть изменяемые глобальные переменные в коде Django, они будут разделяться в пределах одного рабочего процесса, даже между потоками. Вот антипаттерн для демонстрации (никогда не используйте в продакшне):
from django.http import JsonResponse
from django.utils.timezone import now
TIMESTAMPS = [] # разделяемый список
def show_timestamps(request):
TIMESTAMPS.append(now())
return JsonResponse({'timestamps': TIMESTAMPS})Результат после нескольких запросов (вручную отформатирован):
{
"timestamps": [
"2026-06-08T19:48:42.044Z",
"2026-06-08T19:48:43.875Z",
"2026-06-08T19:48:44.776Z"
]
}Если запустить это с Gunicorn и несколькими рабочими процессами, каждый процесс будет иметь свой список TIMESTAMPS, поэтому данные не будут общими между разными воркерами.
При старте Django в память процесса загружаются:
django.apps.registry.Apps (реестр приложений со всеми моделями)urlpatterns)Эта память остаётся выделенной до перезапуска рабочего процесса.
Каждый запрос создаёт объекты, локальные для стека вызовов: view, формы, queryset'ы, сериализаторы и т.д. После отправки ответа:
Изменяемый объект, определённый на уровне модуля, сохраняется между всеми запросами, обрабатываемыми одним рабочим процессом, и будет накапливать данные от каждого пользователя.
❌ ПЛОХО:
_recent_users = []
def dashboard(request):
# утечка между запросами!
_recent_users.append(request.user.id)
return render(request, 'dashboard.html', {'recent': _recent_users})✅ ХОРОШО — состояние хранится в БД, кэше или сессии:
def dashboard(request):
recent_users = get_recent_users_from_db()
return render(request, 'dashboard.html', {'recent': recent_users})Экземпляры middleware живут всё время жизни рабочего процесса. Если хранить что-то специфичное для запроса в self, это будет разделяться между всеми запросами.
❌ ПЛОХО:
class AuditMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.current_user = None # разделяется между запросами!
def __call__(self, request):
self.current_user = request.user
return self.get_response(request)✅ ХОРОШО — прикрепляем данные к объекту запроса:
class AuditMiddleware:
def __init__(self, get_response):
self.get_response = get_response # только неизменяемая конфигурация
def __call__(self, request):
request.current_user = request.user # живёт только в рамках запроса
return self.get_response(request)Атрибут класса разделяется между всеми экземплярами этого класса, а значит, между всеми запросами. Данные первого пользователя могут быть кэшированы и отданы всем остальным.
❌ ПЛОХО:
class ReportGenerator:
_cache = {} # разделяется между всеми экземплярами!
def get_data(self, user):
if 'data' not in self._cache:
self._cache['data'] = expensive_query(user)
return self._cache['data']✅ ХОРОШО — используем фреймворк кэширования с ключом, зависящим от пользователя:
from django.core.cache import cache
class ReportGenerator:
def get_data(self, user):
key = f'report:{user.id}'
return cache.get_or_set(key, lambda: expensive_query(user), timeout=300)Прежде чем писать код на уровне модуля, спросите себя: «Если 1000 разных пользователей обратятся к этому рабочему процессу, и эта переменная будет существовать для всех них, безопасно ли это?»
| Место хранения | Время жизни | Безопасно для пользовательских данных? |
|---|---|---|
| Локальная переменная в view/функции | Один запрос | ✅ Да |
Атрибуты объекта request | Один запрос | ✅ Да |
threading.local() | Один поток | ✅ Да |
| БД / кэш с ключами, зависящими от пользователя | Постоянно, но с ключом | ✅ Да |
self в экземпляре middleware/класса | Время жизни рабочего процесса | ⚠️ Только неизменяемая конфигурация |
| Изменяемая переменная на уровне модуля | Время жизни рабочего процесса | ❌ Никогда не хранить пользовательские данные |
| Изменяемый атрибут класса | Время жизни рабочего процесса | ❌ Никогда не хранить пользовательские данные |
Правило: неизменяемая конфигурация (числа, булевы значения, строки, байты, кортежи, frozenset и None) может жить на уровне модуля; всё, что зависит от запроса, пользователя или времени, должно находиться в рамках цикла запроса.
Теперь вы знаете, что у Django-воркера две области памяти: состояние инициализации (живёт всё время жизни процесса) и состояние запроса (существует только во время обработки). Главное правило: неизменяемая конфигурация — на уровне модуля; всё, что зависит от пользователя или запроса — внутри view или на объекте request. Изменяемые глобальные переменные, атрибуты класса и состояние middleware — источник утечек памяти и уязвимостей. Проверьте свой код: уберите все изменяемые модульные списки и словари, замените их на хранилища с ключами пользователя. Ваше приложение станет надёжнее и безопаснее.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →