diff -pruN 9.0.1-1/.github/workflows/main.yml 9.1.1-1/.github/workflows/main.yml
--- 9.0.1-1/.github/workflows/main.yml	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/.github/workflows/main.yml	2025-04-07 12:45:05.000000000 +0000
@@ -253,6 +253,11 @@ jobs:
       - mypy
       - isort
     runs-on: ubuntu-latest
+    environment:
+      name: pypi
+      url: https://pypi.org/p/django-structlog
+    permissions:
+      id-token: write
     steps:
       - uses: actions/checkout@v4
       - name: Install dependencies
@@ -266,6 +271,3 @@ jobs:
       - name: Publish package
         if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
         uses: pypa/gh-action-pypi-publish@release/v1
-        with:
-          user: __token__
-          password: ${{ secrets.PYPI_DEPLOYMENT_KEY }}
diff -pruN 9.0.1-1/.github/workflows/pre-commit.yml 9.1.1-1/.github/workflows/pre-commit.yml
--- 9.0.1-1/.github/workflows/pre-commit.yml	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/.github/workflows/pre-commit.yml	2025-04-07 12:45:05.000000000 +0000
@@ -19,7 +19,7 @@ jobs:
 
       - uses: browniebroke/pre-commit-autoupdate-action@v1.0.0
 
-      - uses: peter-evans/create-pull-request@v7.0.6
+      - uses: peter-evans/create-pull-request@v7.0.8
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
           branch: update/pre-commit-hooks
diff -pruN 9.0.1-1/.pre-commit-config.yaml 9.1.1-1/.pre-commit-config.yaml
--- 9.0.1-1/.pre-commit-config.yaml	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/.pre-commit-config.yaml	2025-04-07 12:45:05.000000000 +0000
@@ -1,26 +1,26 @@
 repos:
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.9.3
+    rev: v0.11.4
     hooks:
       - id: ruff
         args: [--fix, --exit-non-zero-on-fix]
   - repo: https://github.com/pycqa/isort
-    rev: 5.13.2
+    rev: 6.0.1
     hooks:
       - id: isort
   - repo: https://github.com/ambv/black
-    rev: 24.10.0
+    rev: 25.1.0
     hooks:
       - id: black
         language_version: python3
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.14.1
+    rev: v1.15.0
     hooks:
       - id: mypy
         args: [--no-incremental]
         additional_dependencies: [
             celery-types==0.22.0,
-            "django-stubs[compatible-mypy]==5.1.2",
+            "django-stubs[compatible-mypy]==5.1.3",
             structlog==24.4.0,
             django-extensions==3.2.3,
             django-ipware==7.0.1,
diff -pruN 9.0.1-1/README.rst 9.1.1-1/README.rst
--- 9.0.1-1/README.rst	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/README.rst	2025-04-07 12:45:05.000000000 +0000
@@ -404,7 +404,7 @@ Other libraries alike may be affected by
 Internal changes in how ``RequestMiddleware`` handles exceptions
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-This only affects you if you implemented a middleware inheriting from ``RequestMiddleware`` and you overrided the ``process_exception`` method.
+This only affects you if you implemented a middleware inheriting from ``RequestMiddleware`` and you overrode the ``process_exception`` method.
 
 Did you?
 
diff -pruN 9.0.1-1/compose/local/postgres/Dockerfile 9.1.1-1/compose/local/postgres/Dockerfile
--- 9.0.1-1/compose/local/postgres/Dockerfile	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/compose/local/postgres/Dockerfile	2025-04-07 12:45:05.000000000 +0000
@@ -1,4 +1,4 @@
-FROM postgres:14
+FROM postgres:17
 
 COPY ./compose/local/postgres/maintenance /usr/local/bin/maintenance
 RUN chmod +x /usr/local/bin/maintenance/*
diff -pruN 9.0.1-1/config/settings/base.py 9.1.1-1/config/settings/base.py
--- 9.0.1-1/config/settings/base.py	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/config/settings/base.py	2025-04-07 12:45:05.000000000 +0000
@@ -255,9 +255,9 @@ CELERYD_TASK_SOFT_TIME_LIMIT = 60
 # ------------------------------------------------------------------------------
 ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", True)
 # https://django-allauth.readthedocs.io/en/latest/configuration.html
-ACCOUNT_AUTHENTICATION_METHOD = "username"
+ACCOUNT_LOGIN_METHODS = {"username"}
 # https://django-allauth.readthedocs.io/en/latest/configuration.html
-ACCOUNT_EMAIL_REQUIRED = True
+ACCOUNT_SIGNUP_FIELDS = ["email*", "username*", "password1*", "password2*"]
 # https://django-allauth.readthedocs.io/en/latest/configuration.html
 ACCOUNT_EMAIL_VERIFICATION = "mandatory"
 # https://django-allauth.readthedocs.io/en/latest/configuration.html
diff -pruN 9.0.1-1/config/settings/test.py 9.1.1-1/config/settings/test.py
--- 9.0.1-1/config/settings/test.py	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/config/settings/test.py	2025-04-07 12:45:05.000000000 +0000
@@ -114,3 +114,5 @@ DATABASES = {
 INSTALLED_APPS += ["django_structlog", "test_app"]
 
 DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED = True
+
+IS_WORKER = False
diff -pruN 9.0.1-1/debian/changelog 9.1.1-1/debian/changelog
--- 9.0.1-1/debian/changelog	2025-03-04 21:06:36.000000000 +0000
+++ 9.1.1-1/debian/changelog	2025-08-21 15:41:04.000000000 +0000
@@ -1,3 +1,16 @@
+python-django-structlog (9.1.1-1) unstable; urgency=low
+
+  [ Colin Watson ]
+  * Add "Restrictions: isolation-container" to autopkgtest so that redis can
+    start.
+
+  [ Michael Fladischer ]
+  * New upstream version 9.1.1
+  * Refresh patches.
+  * Bump Standards-Version to 4.7.2.
+
+ -- Michael Fladischer <fladi@debian.org>  Thu, 21 Aug 2025 15:41:04 +0000
+
 python-django-structlog (9.0.1-1) unstable; urgency=low
 
   * New upstream version 9.0.1
diff -pruN 9.0.1-1/debian/control 9.1.1-1/debian/control
--- 9.0.1-1/debian/control	2025-03-04 21:06:36.000000000 +0000
+++ 9.1.1-1/debian/control	2025-08-21 15:41:04.000000000 +0000
@@ -15,7 +15,7 @@ Build-Depends:
  python3-sphinx,
  python3-sphinx-rtd-theme,
  python3-structlog,
-Standards-Version: 4.7.0
+Standards-Version: 4.7.2
 Homepage: https://github.com/jrobichaud/django-structlog
 Vcs-Browser: https://salsa.debian.org/python-team/packages/python-django-structlog
 Vcs-Git: https://salsa.debian.org/python-team/packages/python-django-structlog.git
diff -pruN 9.0.1-1/debian/patches/0001-Remove-remote-badges-to-prevent-privacy-issues.patch 9.1.1-1/debian/patches/0001-Remove-remote-badges-to-prevent-privacy-issues.patch
--- 9.0.1-1/debian/patches/0001-Remove-remote-badges-to-prevent-privacy-issues.patch	2025-03-04 21:06:36.000000000 +0000
+++ 9.1.1-1/debian/patches/0001-Remove-remote-badges-to-prevent-privacy-issues.patch	2025-08-21 15:41:04.000000000 +0000
@@ -7,7 +7,7 @@ Subject: Remove remote badges to prevent
  1 file changed, 64 deletions(-)
 
 diff --git a/README.rst b/README.rst
-index b32e3b1..03359d9 100644
+index 3613d85..0094fbd 100644
 --- a/README.rst
 +++ b/README.rst
 @@ -9,70 +9,6 @@ django-structlog
diff -pruN 9.0.1-1/debian/tests/control 9.1.1-1/debian/tests/control
--- 9.0.1-1/debian/tests/control	2025-03-04 21:06:36.000000000 +0000
+++ 9.1.1-1/debian/tests/control	2025-08-21 15:41:04.000000000 +0000
@@ -9,4 +9,4 @@ Depends:
  redis-server,
  @,
  @builddeps@,
-Restrictions: allow-stderr
+Restrictions: allow-stderr, isolation-container
diff -pruN 9.0.1-1/django_structlog/__init__.py 9.1.1-1/django_structlog/__init__.py
--- 9.0.1-1/django_structlog/__init__.py	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/django_structlog/__init__.py	2025-04-07 12:45:05.000000000 +0000
@@ -1,8 +1,7 @@
-""" ``django-structlog`` is a structured logging integration for ``Django`` project using ``structlog``.
-"""
+"""``django-structlog`` is a structured logging integration for ``Django`` project using ``structlog``."""
 
 name = "django_structlog"
 
-VERSION = (9, 0, 1)
+VERSION = (9, 1, 1)
 
 __version__ = ".".join(str(v) for v in VERSION)
diff -pruN 9.0.1-1/django_structlog/app_settings.py 9.1.1-1/django_structlog/app_settings.py
--- 9.0.1-1/django_structlog/app_settings.py	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/django_structlog/app_settings.py	2025-04-07 12:45:05.000000000 +0000
@@ -12,6 +12,10 @@ class AppSettings:
         return getattr(settings, self.PREFIX + "CELERY_ENABLED", False)
 
     @property
+    def IP_LOGGING_ENABLED(self) -> bool:
+        return getattr(settings, self.PREFIX + "IP_LOGGING_ENABLED", True)
+
+    @property
     def STATUS_4XX_LOG_LEVEL(self) -> int:
         return getattr(settings, self.PREFIX + "STATUS_4XX_LOG_LEVEL", logging.WARNING)
 
diff -pruN 9.0.1-1/django_structlog/celery/__init__.py 9.1.1-1/django_structlog/celery/__init__.py
--- 9.0.1-1/django_structlog/celery/__init__.py	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/django_structlog/celery/__init__.py	2025-04-07 12:45:05.000000000 +0000
@@ -1,2 +1 @@
-""" ``celery`` integration for ``django_structlog``.
-"""
+"""``celery`` integration for ``django_structlog``."""
diff -pruN 9.0.1-1/django_structlog/celery/receivers.py 9.1.1-1/django_structlog/celery/receivers.py
--- 9.0.1-1/django_structlog/celery/receivers.py	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/django_structlog/celery/receivers.py	2025-04-07 12:45:05.000000000 +0000
@@ -1,3 +1,4 @@
+import time
 from typing import TYPE_CHECKING, Any, Optional, Type, cast
 
 import structlog
@@ -93,6 +94,8 @@ class CeleryReceiver:
         signals.bind_extra_task_metadata.send(
             sender=self.receiver_task_prerun, task=task, logger=logger
         )
+        # Record the start time so we can log the task duration later.
+        task.request._django_structlog_started_at = time.monotonic_ns()
         logger.info("task_started", task=task.name)
 
     def receiver_task_retry(
@@ -105,12 +108,15 @@ class CeleryReceiver:
         logger.warning("task_retrying", reason=reason)
 
     def receiver_task_success(
-        self, result: Optional[str] = None, **kwargs: Any
+        self, result: Optional[str] = None, sender: Optional[Any] = None, **kwargs: Any
     ) -> None:
         signals.pre_task_succeeded.send(
             sender=self.receiver_task_success, logger=logger, result=result
         )
-        logger.info("task_succeeded")
+
+        log_vars: dict[str, Any] = {}
+        self.add_duration_ms(sender, log_vars)
+        logger.info("task_succeeded", **log_vars)
 
     def receiver_task_failure(
         self,
@@ -122,17 +128,31 @@ class CeleryReceiver:
         *args: Any,
         **kwargs: Any,
     ) -> None:
+        log_vars: dict[str, Any] = {}
+        self.add_duration_ms(sender, log_vars)
         throws = getattr(sender, "throws", ())
         if isinstance(exception, throws):
             logger.info(
                 "task_failed",
                 error=str(exception),
+                **log_vars,
             )
         else:
             logger.exception(
                 "task_failed",
                 error=str(exception),
                 exception=exception,
+                **log_vars,
+            )
+
+    @classmethod
+    def add_duration_ms(
+        cls, task: Optional[Type[Any]], log_vars: dict[str, Any]
+    ) -> None:
+        if task and hasattr(task.request, "_django_structlog_started_at"):
+            started_at: int = task.request._django_structlog_started_at
+            log_vars["duration_ms"] = round(
+                (time.monotonic_ns() - started_at) / 1_000_000
             )
 
     def receiver_task_revoked(
diff -pruN 9.0.1-1/django_structlog/middlewares/request.py 9.1.1-1/django_structlog/middlewares/request.py
--- 9.0.1-1/django_structlog/middlewares/request.py	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/django_structlog/middlewares/request.py	2025-04-07 12:45:05.000000000 +0000
@@ -21,6 +21,7 @@ from asgiref import sync
 from django.core.exceptions import PermissionDenied
 from django.core.signals import got_request_exception
 from django.http import Http404, StreamingHttpResponse
+from django.utils.functional import SimpleLazyObject
 
 from .. import signals
 from ..app_settings import app_settings
@@ -39,6 +40,7 @@ else:
 if TYPE_CHECKING:  # pragma: no cover
     from types import TracebackType
 
+    from django.contrib.auth.base_user import AbstractBaseUser
     from django.http import HttpRequest, HttpResponse
 
 logger = structlog.getLogger(__name__)
@@ -179,8 +181,6 @@ class RequestMiddleware:
         structlog.contextvars.clear_contextvars()
 
     def prepare(self, request: "HttpRequest") -> None:
-        from ipware import get_client_ip  # type: ignore[import-untyped]
-
         request_id = get_request_header(
             request, "x-request-id", "HTTP_X_REQUEST_ID"
         ) or str(uuid.uuid4())
@@ -191,8 +191,8 @@ class RequestMiddleware:
         self.bind_user_id(request)
         if correlation_id:
             structlog.contextvars.bind_contextvars(correlation_id=correlation_id)
-        ip, _ = get_client_ip(request)
-        structlog.contextvars.bind_contextvars(ip=ip)
+        if app_settings.IP_LOGGING_ENABLED:
+            self.bind_ip(request)
         log_kwargs = {
             "request": self.format_request(request),
             "user_agent": request.META.get("HTTP_USER_AGENT"),
@@ -202,6 +202,13 @@ class RequestMiddleware:
         )
         logger.info("request_started", **log_kwargs)
 
+    @classmethod
+    def bind_ip(cls, request: "HttpRequest") -> None:
+        from ipware import get_client_ip  # type: ignore[import-untyped]
+
+        ip, _ = get_client_ip(request)
+        structlog.contextvars.bind_contextvars(ip=ip)
+
     @staticmethod
     def format_request(request: "HttpRequest") -> str:
         return f"{request.method} {request.get_full_path()}"
@@ -209,7 +216,14 @@ class RequestMiddleware:
     @staticmethod
     def bind_user_id(request: "HttpRequest") -> None:
         user_id_field = app_settings.USER_ID_FIELD
-        if hasattr(request, "user") and request.user is not None and user_id_field:
+        if not user_id_field or not hasattr(request, "user"):
+            return
+
+        session_was_accessed = (
+            request.session.accessed if hasattr(request, "session") else None
+        )
+
+        if request.user is not None:
             user_id = None
             if hasattr(request.user, user_id_field):
                 user_id = getattr(request.user, user_id_field)
@@ -217,6 +231,17 @@ class RequestMiddleware:
                     user_id = str(user_id)
             structlog.contextvars.bind_contextvars(user_id=user_id)
 
+        if session_was_accessed is False:
+            """using SessionMiddleware but user was never accessed, must reset accessed state"""
+            user = request.user
+
+            def get_user() -> Any:
+                request.session.accessed = True
+                return user
+
+            request.user = cast("AbstractBaseUser", SimpleLazyObject(get_user))
+            request.session.accessed = False
+
     def process_got_request_exception(
         self, sender: Type[Any], request: "HttpRequest", **kwargs: Any
     ) -> None:
diff -pruN 9.0.1-1/docker-compose.yml 9.1.1-1/docker-compose.yml
--- 9.0.1-1/docker-compose.yml	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/docker-compose.yml	2025-04-07 12:45:05.000000000 +0000
@@ -51,7 +51,7 @@ services:
       - ./.envs/.local/.postgres
 
   redis:
-    image: redis:3.2
+    image: redis:7.4
     ports:
       - "6379:6379"
 
diff -pruN 9.0.1-1/docs/changelog.rst 9.1.1-1/docs/changelog.rst
--- 9.0.1-1/docs/changelog.rst	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/docs/changelog.rst	2025-04-07 12:45:05.000000000 +0000
@@ -1,7 +1,28 @@
 Change Log
 ==========
 
-9.0.1 (January 29, 2024)
+9.1.1 (April 7, 2025)
+------------------------
+
+*Fixes:*
+    - fix ``duration_ms`` to the celery tasks metadata not working. See `#811 <https://github.com/jrobichaud/django-structlog/pull/811>`_. Special thanks to `@ahumeau <https://github.com/ahumeau>`_.
+
+
+9.1.0 (April 4, 2025)
+------------------------
+
+*New:*
+    - new setting ``DJANGO_STRUCTLOG_IP_LOGGING_ENABLED`` (default ``True``) to allow to disable ip binding. See `#803 <https://github.com/jrobichaud/django-structlog/issues/803>`_. Special thanks to `@dulguunpc <https://github.com/dulguunpc>`_.
+    - add ``duration_ms`` to the celery tasks metadata. See `#796 <https://github.com/jrobichaud/django-structlog/pull/796>`_. Special thanks to `@ahumeau <https://github.com/ahumeau>`_.
+
+*Fixes:*
+    - `vary: Cookie` was always set. See `#734 <https://github.com/jrobichaud/django-structlog/issues/734>`_. Special thanks to `@last-partizan <https://github.com/last-partizan>`_
+
+*Other:*
+    - add support of django 5.2 (just adding it to the test matrix, no code changes)
+
+
+9.0.1 (January 29, 2025)
 ------------------------
 
 *Fixes:*
diff -pruN 9.0.1-1/docs/configuration.rst 9.1.1-1/docs/configuration.rst
--- 9.0.1-1/docs/configuration.rst	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/docs/configuration.rst	2025-04-07 12:45:05.000000000 +0000
@@ -14,6 +14,7 @@ Example:
 
 
 .. _settings:
+
 Settings
 --------
 
@@ -22,9 +23,11 @@ Settings
 +==========================================+=========+=================+===============================================================================+
 | DJANGO_STRUCTLOG_CELERY_ENABLED          | boolean | False           | See :ref:`celery_integration`                                                 |
 +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+
+| DJANGO_STRUCTLOG_IP_LOGGING_ENABLED      | boolean | True            | automatically bind user ip using `django-ipware`                              |
++------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+
 | DJANGO_STRUCTLOG_STATUS_4XX_LOG_LEVEL    | int     | logging.WARNING | Log level of 4XX status codes                                                 |
 +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+
 | DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED | boolean | False           | See :ref:`commands`                                                           |
 +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+
-| DJANGO_STRUCTLOG_USER_ID_FIELD           | boolean | ``"pk"``        | Change field used to identify user in logs, ``None`` to disable user binding  |
+| DJANGO_STRUCTLOG_USER_ID_FIELD           | string  | ``"pk"``        | Change field used to identify user in logs, ``None`` to disable user binding  |
 +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+
diff -pruN 9.0.1-1/docs/events.rst 9.1.1-1/docs/events.rst
--- 9.0.1-1/docs/events.rst	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/docs/events.rst	2025-04-07 12:45:05.000000000 +0000
@@ -154,10 +154,14 @@ These metadata appear once along with th
 +------------------+------------------+----------------------------------------+
 | task_started     | task             | name of the task                       |
 +------------------+------------------+----------------------------------------+
+| task_succeeded   | duration_ms      | duration of the task in milliseconds   |
++------------------+------------------+----------------------------------------+
 | task_failed      | error            | exception as string                    |
 +------------------+------------------+----------------------------------------+
 | task_failed      | exception*       | exception's traceback                  |
 +------------------+------------------+----------------------------------------+
+| task_failed      | duration_ms      | duration of the task in milliseconds   |
++------------------+------------------+----------------------------------------+
 | task_revoked     | terminated       | Set to True if the task was terminated |
 +------------------+------------------+----------------------------------------+
 | task_revoked     | signum           | python termination signal's number     |
diff -pruN 9.0.1-1/docs/requirements.txt 9.1.1-1/docs/requirements.txt
--- 9.0.1-1/docs/requirements.txt	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/docs/requirements.txt	2025-04-07 12:45:05.000000000 +0000
@@ -1,8 +1,8 @@
-sphinx==8.1.3
+sphinx==8.2.3
 sphinx_rtd_theme==3.0.2
 celery==5.4.0
 django>=4.2,<6
 structlog
 sphinx-autobuild==2024.10.3
-Jinja2==3.1.5
+Jinja2==3.1.6
 importlib-metadata>=8.0.0,<9
diff -pruN 9.0.1-1/pyproject.toml 9.1.1-1/pyproject.toml
--- 9.0.1-1/pyproject.toml	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/pyproject.toml	2025-04-07 12:45:05.000000000 +0000
@@ -24,6 +24,7 @@ build-backend = "setuptools.build_meta"
     "Framework :: Django :: 4.2",
     "Framework :: Django :: 5.0",
     "Framework :: Django :: 5.1",
+    "Framework :: Django :: 5.2",
     "Programming Language :: Python :: 3",
     "Programming Language :: Python :: 3 :: Only",
     "Programming Language :: Python :: 3.9",
@@ -102,9 +103,9 @@ build-backend = "setuptools.build_meta"
     # Also, make sure that all python versions used here are included in ./github/worksflows/main.yml
     envlist =
         py{39,310,311}-django42-celery5{2,3}-redis{3,4}-kombu5,
-        py31{0,1}-django5{0,1}-celery5{3,4}-redis4-kombu5,
-        py312-django{42,50,51}-celery5{3,4}-redis4-kombu5,
-        py313-django{51}-celery5{3,4}-redis4-kombu5,
+        py31{0,1}-django5{0,1,2}-celery5{3,4}-redis4-kombu5,
+        py312-django{42,50,51,52}-celery5{3,4}-redis4-kombu5,
+        py313-django5{1,2}-celery5{3,4}-redis4-kombu5,
 
     [gh-actions]
     python =
@@ -132,6 +133,7 @@ build-backend = "setuptools.build_meta"
         django42: Django >=4.2, <5.0
         django50: Django >=5.0, <5.1
         django51: Django >=5.1, <5.2
+        django52: Django >=5.2, <6.0
         -r{toxinidir}/requirements/ci.txt
 
     commands = pytest --cov=./test_app --cov=./django_structlog --cov-append test_app
diff -pruN 9.0.1-1/requirements/black.txt 9.1.1-1/requirements/black.txt
--- 9.0.1-1/requirements/black.txt	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/requirements/black.txt	2025-04-07 12:45:05.000000000 +0000
@@ -1 +1 @@
-black==24.10.0  # https://github.com/ambv/black
+black==25.1.0  # https://github.com/ambv/black
diff -pruN 9.0.1-1/requirements/ci.txt 9.1.1-1/requirements/ci.txt
--- 9.0.1-1/requirements/ci.txt	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/requirements/ci.txt	2025-04-07 12:45:05.000000000 +0000
@@ -7,13 +7,13 @@ django-extensions==3.2.3
 structlog>=21.4.0
 colorama>=0.4.3
 
-psycopg[binary]==3.2.4 # https://github.com/psycopg/psycopg
+psycopg[binary]==3.2.6 # https://github.com/psycopg/psycopg
 
 # Testing
 # ------------------------------------------------------------------------------
-pytest==8.3.4   # https://github.com/pytest-dev/pytest
+pytest==8.3.5   # https://github.com/pytest-dev/pytest
 pytest-sugar==1.0.0  # https://github.com/Frozenball/pytest-sugar
-pytest-cov==6.0.0
+pytest-cov==6.1.1
 
 # Code quality
 # ------------------------------------------------------------------------------
@@ -23,10 +23,10 @@ pylint-celery==0.3  # https://github.com
 
 # Django
 # ------------------------------------------------------------------------------
-factory-boy==3.3.1  # https://github.com/FactoryBoy/factory_boy
+factory-boy==3.3.3  # https://github.com/FactoryBoy/factory_boy
 
 django-coverage-plugin==3.1.0  # https://github.com/nedbat/django_coverage_plugin
-pytest-django==4.9.0  # https://github.com/pytest-dev/pytest-django
+pytest-django==4.11.1  # https://github.com/pytest-dev/pytest-django
 
 # Setup tools
 # ------------------------------------------------------------------------------
diff -pruN 9.0.1-1/requirements/coverage.txt 9.1.1-1/requirements/coverage.txt
--- 9.0.1-1/requirements/coverage.txt	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/requirements/coverage.txt	2025-04-07 12:45:05.000000000 +0000
@@ -1 +1 @@
-coverage==7.6.10  # https://github.com/nedbat/coveragepy
+coverage==7.8.0  # https://github.com/nedbat/coveragepy
diff -pruN 9.0.1-1/requirements/local-base.txt 9.1.1-1/requirements/local-base.txt
--- 9.0.1-1/requirements/local-base.txt	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/requirements/local-base.txt	2025-04-07 12:45:05.000000000 +0000
@@ -1,38 +1,38 @@
-pytz==2024.2  # https://github.com/stub42/pytz
+pytz==2025.2  # https://github.com/stub42/pytz
 python-slugify==8.0.4  # https://github.com/un33k/python-slugify
 
 # Django
 # ------------------------------------------------------------------------------
-django==5.1.5  # https://www.djangoproject.com/
+django==5.2.0  # https://www.djangoproject.com/
 django-environ==0.12.0  # https://github.com/joke2k/django-environ
 django-model-utils==5.0.0  # https://github.com/jazzband/django-model-utils
-django-allauth==65.3.1  # https://github.com/pennersr/django-allauth
+django-allauth==65.6.0  # https://github.com/pennersr/django-allauth
 django-crispy-forms==2.3  # https://github.com/django-crispy-forms/django-crispy-forms
-crispy-bootstrap5==2024.10 # https://github.com/django-crispy-forms/crispy-bootstrap5
+crispy-bootstrap5==2025.4 # https://github.com/django-crispy-forms/crispy-bootstrap5
 django-redis==5.4.0  # https://github.com/niwinz/django-redis
 asgiref==3.8.1 # https://github.com/django/asgiref
 
 # Django REST Framework
-djangorestframework==3.15.2  # https://github.com/encode/django-rest-framework
+djangorestframework==3.16.0  # https://github.com/encode/django-rest-framework
 coreapi==2.3.3  # https://github.com/core-api/python-client
 
 # django-ninja
 django-ninja==1.3.0  # https://github.com/vitalik/django-ninja
 
-structlog==25.1.0
+structlog==25.2.0
 colorama==0.4.6
 django-ipware==7.0.1
 
 Werkzeug==3.1.3  # https://github.com/pallets/werkzeug
 ipdb==0.13.13  # https://github.com/gotcha/ipdb
-psycopg[binary]==3.2.4 # https://github.com/psycopg/psycopg
+psycopg[binary]==3.2.6 # https://github.com/psycopg/psycopg
 
 # Testing
 # ------------------------------------------------------------------------------
-pytest==8.3.4   # https://github.com/pytest-dev/pytest
+pytest==8.3.5   # https://github.com/pytest-dev/pytest
 pytest-sugar==1.0.0  # https://github.com/Frozenball/pytest-sugar
-pytest-cov==6.0.0
-pytest-asyncio==0.25.2 # https://github.com/pytest-dev/pytest-asyncio
+pytest-cov==6.1.1
+pytest-asyncio==0.26.0 # https://github.com/pytest-dev/pytest-asyncio
 pytest-mock==3.14.0 # https://github.com/pytest-dev/pytest-mock
 
 # Code quality
@@ -45,12 +45,12 @@ pylint-celery==0.3  # https://github.com
 
 # Django
 # ------------------------------------------------------------------------------
-factory-boy==3.3.1  # https://github.com/FactoryBoy/factory_boy
+factory-boy==3.3.3  # https://github.com/FactoryBoy/factory_boy
 
 django-extensions==3.2.3  # https://github.com/django-extensions/django-extensions
 django-coverage-plugin==3.1.0  # https://github.com/nedbat/django_coverage_plugin
-pytest-django==4.9.0  # https://github.com/pytest-dev/pytest-django
+pytest-django==4.11.1  # https://github.com/pytest-dev/pytest-django
 
 # pre-commit
 # ------------------------------------------------------------------------------
-pre-commit==4.1.0  # https://github.com/pre-commit/pre-commit
+pre-commit==4.2.0  # https://github.com/pre-commit/pre-commit
diff -pruN 9.0.1-1/requirements/local.txt 9.1.1-1/requirements/local.txt
--- 9.0.1-1/requirements/local.txt	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/requirements/local.txt	2025-04-07 12:45:05.000000000 +0000
@@ -2,7 +2,7 @@
 
 redis==5.2.1  # https://github.com/antirez/redis
 celery==5.4.0  # pyup: < 5.0  # https://github.com/celery/celery
-kombu==5.4.2
+kombu==5.5.2
 flower==2.0.1  # https://github.com/mher/flower
 uvicorn==0.34.0 # https://github.com/encode/uvicorn
 gunicorn==23.0.0 # https://github.com/benoitc/gunicorn
diff -pruN 9.0.1-1/requirements/mypy.txt 9.1.1-1/requirements/mypy.txt
--- 9.0.1-1/requirements/mypy.txt	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/requirements/mypy.txt	2025-04-07 12:45:05.000000000 +0000
@@ -1,3 +1,3 @@
-mypy==1.14.1
-celery-types==0.22.0
-django-stubs[compatible-mypy]==5.1.2
+mypy==1.15.0
+celery-types==0.23.0
+django-stubs[compatible-mypy]==5.1.3
diff -pruN 9.0.1-1/requirements/ruff.txt 9.1.1-1/requirements/ruff.txt
--- 9.0.1-1/requirements/ruff.txt	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/requirements/ruff.txt	2025-04-07 12:45:05.000000000 +0000
@@ -1 +1 @@
-ruff==0.9.3  # https://github.com/astral-sh/ruff
+ruff==0.11.4  # https://github.com/astral-sh/ruff
diff -pruN 9.0.1-1/test_app/tests/celery/test_receivers.py 9.1.1-1/test_app/tests/celery/test_receivers.py
--- 9.0.1-1/test_app/tests/celery/test_receivers.py	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/test_app/tests/celery/test_receivers.py	2025-04-07 12:45:05.000000000 +0000
@@ -1,4 +1,5 @@
 import logging
+import time
 from signal import SIGTERM
 from typing import Any, Optional, Type
 from unittest.mock import MagicMock, Mock, call, patch
@@ -288,19 +289,39 @@ class TestReceivers(TestCase):
         ) -> None:
             structlog.contextvars.bind_contextvars(result=result)
 
+        mock_sender = Mock()
+        mock_sender.request._django_structlog_started_at = time.monotonic()
+
         receiver = receivers.CeleryReceiver()
         with self.assertLogs(
             logging.getLogger("django_structlog.celery.receivers"), logging.INFO
         ) as log_results:
-            receiver.receiver_task_success(result=expected_result)
+            receiver.receiver_task_success(result=expected_result, sender=mock_sender)
 
         self.assertEqual(1, len(log_results.records))
         record: Any = log_results.records[0]
         self.assertEqual("task_succeeded", record.msg["event"])
         self.assertEqual("INFO", record.levelname)
         self.assertIn("result", record.msg)
+        self.assertIn("duration_ms", record.msg)
+        self.assertGreaterEqual(record.msg["duration_ms"], 0)
         self.assertEqual(expected_result, record.msg["result"])
 
+    def test_receiver_task_success_no_started_at(self) -> None:
+        expected_result = "foo"
+
+        receiver = receivers.CeleryReceiver()
+        with self.assertLogs(
+            logging.getLogger("django_structlog.celery.receivers"), logging.INFO
+        ) as log_results:
+            receiver.receiver_task_success(result=expected_result)
+
+        self.assertEqual(1, len(log_results.records))
+        record: Any = log_results.records[0]
+        self.assertEqual("task_succeeded", record.msg["event"])
+        self.assertEqual("INFO", record.levelname)
+        self.assertNotIn("duration_ms", record.msg)
+
     def test_receiver_task_failure(self) -> None:
         expected_exception = "foo"
         receiver = receivers.CeleryReceiver()
@@ -322,6 +343,7 @@ class TestReceivers(TestCase):
 
         mock_sender = Mock()
         mock_sender.throws = (Exception,)
+        mock_sender.request._django_structlog_started_at = time.monotonic()
         receiver = receivers.CeleryReceiver()
         with self.assertLogs(
             logging.getLogger("django_structlog.celery.receivers"), logging.INFO
@@ -335,6 +357,7 @@ class TestReceivers(TestCase):
         self.assertEqual("task_failed", record.msg["event"])
         self.assertEqual("INFO", record.levelname)
         self.assertIn("error", record.msg)
+        self.assertIn("duration_ms", record.msg)
         self.assertNotIn("exception", record.msg)
         self.assertEqual(expected_exception, record.msg["error"])
 
diff -pruN 9.0.1-1/test_app/tests/middlewares/test_request.py 9.1.1-1/test_app/tests/middlewares/test_request.py
--- 9.0.1-1/test_app/tests/middlewares/test_request.py	2025-01-29 21:01:45.000000000 +0000
+++ 9.1.1-1/test_app/tests/middlewares/test_request.py	2025-04-07 12:45:05.000000000 +0000
@@ -7,7 +7,9 @@ from unittest import mock
 from unittest.mock import AsyncMock, Mock, patch
 
 import structlog
+from django.contrib.auth.middleware import AuthenticationMiddleware
 from django.contrib.auth.models import AnonymousUser, User
+from django.contrib.sessions.middleware import SessionMiddleware
 from django.contrib.sites.models import Site
 from django.contrib.sites.shortcuts import get_current_site
 from django.core.exceptions import PermissionDenied
@@ -236,6 +238,97 @@ class TestRequestMiddleware(TestCase):
         self.assertIn("user_id", record.msg)
         self.assertIsNone(record.msg["user_id"])
 
+    @override_settings(
+        SECRET_KEY="00000000000000000000000000000000",
+    )
+    def test_process_request_session_middleware_without_vary(self) -> None:
+        def get_response(_request: HttpRequest) -> HttpResponse:
+            with self.assertLogs(__name__, logging.INFO) as log_results:
+                self.logger.info("hello")
+            self.log_results = log_results
+            return HttpResponse()
+
+        request = self.factory.get("/foo")
+
+        # simulate SessionMiddleware, AuthenticationMiddleware, and RequestMiddleware called in that order
+        request_middleware = RequestMiddleware(get_response)
+        authentication_middleware = AuthenticationMiddleware(
+            cast(
+                Any,
+                lambda r: request_middleware(r),
+            )
+        )
+        session_middleware = SessionMiddleware(
+            cast(Any, lambda r: authentication_middleware(r))
+        )
+        response = session_middleware(request)
+
+        self.assertEqual(1, len(self.log_results.records))
+        record = self.log_results.records[0]
+        self.assertIsNone(cast(HttpResponse, response).headers.get("Vary"))
+
+        self.assertEqual("INFO", record.levelname)
+
+        self.assertIn("user_id", record.msg)
+        self.assertIsNone(record.msg["user_id"])
+
+    @override_settings(
+        SECRET_KEY="00000000000000000000000000000000",
+    )
+    def test_process_request_session_middleware_with_vary(self) -> None:
+        def get_response(_request: HttpRequest) -> HttpResponse:
+            assert isinstance(
+                request.user, AnonymousUser
+            )  # force evaluate user to trigger session middleware
+            with self.assertLogs(__name__, logging.INFO) as log_results:
+                self.logger.info("hello")
+            self.log_results = log_results
+            return HttpResponse()
+
+        request = self.factory.get("/foo")
+
+        # simulate SessionMiddleware, AuthenticationMiddleware, and RequestMiddleware called in that order
+        request_middleware = RequestMiddleware(get_response)
+        authentication_middleware = AuthenticationMiddleware(
+            cast(Any, lambda r: request_middleware(r))
+        )
+        session_middleware = SessionMiddleware(
+            cast(Any, lambda r: authentication_middleware(r))
+        )
+        response = session_middleware(request)
+
+        self.assertEqual(1, len(self.log_results.records))
+        record = self.log_results.records[0]
+        self.assertIsNotNone(cast(HttpResponse, response).headers.get("Vary"))
+
+        self.assertEqual("INFO", record.levelname)
+
+        self.assertIn("user_id", record.msg)
+        self.assertIsNone(record.msg["user_id"])
+
+    @override_settings(
+        DJANGO_STRUCTLOG_USER_ID_FIELD=None,
+    )
+    def test_process_request_no_user_id_field(self) -> None:
+        def get_response(_request: HttpRequest) -> HttpResponse:
+            with self.assertLogs(__name__, logging.INFO) as log_results:
+                self.logger.info("hello")
+            self.log_results = log_results
+            return HttpResponse()
+
+        request = self.factory.get("/foo")
+
+        middleware = RequestMiddleware(get_response)
+        response = middleware(request)
+        self.assertEqual(200, cast(HttpResponse, response).status_code)
+
+        self.assertEqual(1, len(self.log_results.records))
+        record = self.log_results.records[0]
+
+        self.assertEqual("INFO", record.levelname)
+
+        self.assertNotIn("user_id", record.msg)
+
     def test_log_user_in_request_finished(self) -> None:
         mock_response = Mock()
         mock_response.status_code = 200
@@ -901,6 +994,31 @@ class TestRequestMiddleware(TestCase):
         record: Any = log_results.records[0]
         self.assertEqual("request_cancelled", record.msg["event"])
 
+    @override_settings(DJANGO_STRUCTLOG_IP_LOGGING_ENABLED=False)
+    def test_disable_ip_logging(self) -> None:
+        mock_response = Mock()
+        mock_response.status_code = 200
+
+        def get_response(_request: HttpRequest) -> HttpResponse:
+            with self.assertLogs(__name__, logging.INFO) as log_results:
+                self.logger.info("hello")
+            self.log_results = log_results
+            return mock_response
+
+        request = RequestFactory().get("/foo")
+
+        middleware = RequestMiddleware(get_response)
+        middleware(request)
+
+        self.assertEqual(1, len(self.log_results.records))
+        record: Any
+        record = self.log_results.records[0]
+
+        self.assertEqual("INFO", record.levelname)
+        self.assertIn("request_id", record.msg)
+        self.assertNotIn("user_id", record.msg)
+        self.assertNotIn("ip", record.msg)
+
 
 class TestRequestMiddlewareRouter(TestCase):
     async def test_async(self) -> None:
