diff --git a/debug_toolbar/panels/cache.py b/debug_toolbar/panels/cache.py
index 24c942de7..20b2faa9b 100644
--- a/debug_toolbar/panels/cache.py
+++ b/debug_toolbar/panels/cache.py
@@ -116,7 +116,13 @@ def _store_call_info(
template_info,
backend,
):
- if name == "get" or name == "get_or_set":
+ if name == "get":
+ default = kwargs.get("default", args[1] if len(args) > 1 else None)
+ if return_value == default:
+ self.misses += 1
+ else:
+ self.hits += 1
+ elif name == "get_or_set":
if return_value is None:
self.misses += 1
else:
@@ -143,7 +149,7 @@ def _store_call_info(
def _record_call(self, cache, alias, name, original_method, args, kwargs):
# Some cache backends implement certain cache methods in terms of other cache
- # methods (e.g. get_or_set() in terms of get() and add()). In order to only
+ # methods (e.g. get_or_set() in terms of get() and add()). In order to only
# record the calls made directly by the user code, set the cache's _djdt_panel
# attribute to None before invoking the original method, which will cause the
# monkey-patched cache methods to skip recording additional calls made during
diff --git a/debug_toolbar/static/debug_toolbar/css/toolbar.css b/debug_toolbar/static/debug_toolbar/css/toolbar.css
index ad0d58df9..a7000a95a 100644
--- a/debug_toolbar/static/debug_toolbar/css/toolbar.css
+++ b/debug_toolbar/static/debug_toolbar/css/toolbar.css
@@ -348,7 +348,14 @@
height: 50px;
}
-#djDebug .djDebugPanelTitle code {
+#djDebug .djdt-cache-note {
+ margin: 1em 0;
+ padding: 8px 10px;
+ border: 1px solid var(--djdt-table-border-color);
+ background-color: var(--djdt-panel-content-table-strip-background-color);
+}
+#djDebug .djDebugPanelTitle code,
+#djDebug .djdt-cache-note code {
display: inline;
font-size: inherit;
}
diff --git a/debug_toolbar/templates/debug_toolbar/panels/cache.html b/debug_toolbar/templates/debug_toolbar/panels/cache.html
index 04c7d37a1..f2ebe8c3d 100644
--- a/debug_toolbar/templates/debug_toolbar/panels/cache.html
+++ b/debug_toolbar/templates/debug_toolbar/panels/cache.html
@@ -18,6 +18,11 @@
{% translate "Summary" %}
+
+ Cache hit/miss statistics for cache.get() calls may not always be accurate.
+ See the discussion
+ for details and to share feedback about improving cache hit/miss tracking.
+
{% translate "Commands" %}
diff --git a/docs/changes.rst b/docs/changes.rst
index 5b3156bbc..f2c9c89b8 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -21,6 +21,9 @@ Pending
* Stopped the history panel buttons from submitting their form when clicked
before the panel script has loaded, which navigated away from the page.
* Added support for Django 6.1.
+* Improved cache hit/miss reporting in the Cache panel for ``cache.get()``
+ calls with a supplied default value, while documenting the remaining
+ ambiguity when a cached value equals the supplied default.
7.0.0 (2026-06-17)
------------------
diff --git a/docs/panels.rst b/docs/panels.rst
index c9ce5ab63..e6d0e5f26 100644
--- a/docs/panels.rst
+++ b/docs/panels.rst
@@ -101,6 +101,12 @@ Cache
Cache queries. Is incompatible with Django's per-site caching.
+Cache hit/miss statistics for ``cache.get()`` calls may not always be
+accurate. See the `discussion`_ for details and to share feedback about
+improving cache hit/miss tracking.
+
+.. _discussion: https://github.com/django-commons/django-debug-toolbar/discussions/2441
+
Signals
~~~~~~~
diff --git a/tests/panels/test_cache.py b/tests/panels/test_cache.py
index fff28280b..66f86d384 100644
--- a/tests/panels/test_cache.py
+++ b/tests/panels/test_cache.py
@@ -45,6 +45,58 @@ def test_hits_and_misses(self):
self.assertEqual(self.panel.hits, 4)
self.assertEqual(self.panel.misses, 2)
+ def test_cached_none_with_default_none_is_ambiguous(self):
+ cache.cache.clear()
+
+ cache.cache.set("foo", None)
+ self.assertIsNone(cache.cache.get("foo"))
+ self.assertEqual(self.panel.hits, 0)
+ self.assertEqual(self.panel.misses, 1)
+
+ def test_cached_value_equal_to_default_is_ambiguous(self):
+ cache.cache.clear()
+
+ cache.cache.set("foo", "bar")
+ self.assertEqual(cache.cache.get("foo", "bar"), "bar")
+ self.assertEqual(self.panel.hits, 0)
+ self.assertEqual(self.panel.misses, 1)
+
+ def test_missing_key_with_default_counts_as_miss(self):
+ cache.cache.clear()
+
+ self.assertIsNone(cache.cache.get("foo", None))
+ self.assertEqual(self.panel.hits, 0)
+ self.assertEqual(self.panel.misses, 1)
+
+ def test_missing_key_returns_supplied_default(self):
+ cache.cache.clear()
+
+ self.assertEqual(cache.cache.get("foo", "bar"), "bar")
+ self.assertEqual(self.panel.hits, 0)
+ self.assertEqual(self.panel.misses, 1)
+
+ def test_get_with_keyword_default_is_cache_miss(self):
+ cache.cache.clear()
+
+ self.assertEqual(cache.cache.get("foo", default="default"), "default")
+ self.assertEqual(self.panel.hits, 0)
+ self.assertEqual(self.panel.misses, 1)
+
+ def test_get_with_keyword_key_and_default_is_cache_miss(self):
+ cache.cache.clear()
+
+ self.assertEqual(cache.cache.get(key="foo", default="default"), "default")
+ self.assertEqual(self.panel.hits, 0)
+ self.assertEqual(self.panel.misses, 1)
+
+ def test_get_with_keyword_key_and_version(self):
+ cache.cache.clear()
+
+ cache.cache.set("foo", "bar", version=1)
+ self.assertEqual(cache.cache.get(key="foo", version=1), "bar")
+ self.assertEqual(self.panel.hits, 1)
+ self.assertEqual(self.panel.misses, 0)
+
def test_get_or_set_value(self):
cache.cache.get_or_set("baz", "val")
self.assertEqual(cache.cache.get("baz"), "val")
@@ -114,6 +166,13 @@ def test_get_or_set_does_not_override_existing_value(self):
},
)
+ def test_get_or_set_none_counts_as_miss(self):
+ cache.cache.clear()
+
+ self.assertIsNone(cache.cache.get_or_set("foo", None))
+ self.assertEqual(self.panel.hits, 0)
+ self.assertEqual(self.panel.misses, 1)
+
def test_insert_content(self):
"""
Test that the panel only inserts content after generate_stats and