CyberHuginn

Home

/

Notes

/

building-django-healthkit-02-database-cache-checks

Building a Django Package — Part 2: Database and Cache Health Checks

In this part, we implement the first health checks for django-healthkit, covering database connectivity and Django cache functionality with simple, independent, and testable checks.

Django

Packages

OpenSource

Python

Aug 20, 2026 · 6 min read

In the previous part, we created the initial structure of django-healthkit, configured pyproject.toml, and installed the package in editable mode.

At this point, the package is installable, but it doesn't actually check anything yet.

In this part, we will start implementing the core functionality of the package by creating two health checks:

  • Database
  • Cache

The goal is to keep these checks small, independent, and easy to extend later.


Designing the Check Structure

Before writing any checks, let's create a dedicated package for them.

Update the project structure:

django-healthkit/
├── src/
│   └── django_healthkit/
│       ├── __init__.py
│       ├── apps.py
│       ├── checks/
│       │   ├── __init__.py
│       │   ├── database.py
│       │   └── cache.py
│       └── migrations/
│           └── __init__.py
├── tests/
├── .gitignore
├── LICENSE
├── README.md
└── pyproject.toml

The important part here is that every health check gets its own module.

This makes the package easier to maintain as the number of checks grows.

For example, later we can have:

checks/
├── database.py
├── cache.py
├── disk.py
├── memory.py
└── cpu.py

Each module should have one responsibility: determine whether a specific system component is working correctly.


What Should a Health Check Return?

We need a consistent result from our checks.

For the first version, we can keep things simple and return a dictionary:

{
    "status": "ok"
}

If something goes wrong:

{
    "status": "error",
    "error": "..."
}

This gives us a simple contract that can later be used by a health endpoint.

For example, the final response could eventually look like:

{
    "status": "ok",
    "checks": {
        "database": {
            "status": "ok"
        },
        "cache": {
            "status": "ok"
        }
    }
}

We will build the endpoint around this structure in a later part.


Database Health Check

The database is usually one of the most important dependencies of a Django application.

If the application server is running but the database is unavailable, the application is effectively unhealthy.

Django manages database connections for us, so we don't need to manually construct a connection.

Instead, we can use Django's database connection API.

Create:

src/django_healthkit/checks/database.py

with:

from django.db import connection


def check_database():
    try:
        connection.ensure_connection()

        return {
            "status": "ok",
        }

    except Exception as exc:
        return {
            "status": "error",
            "error": str(exc),
        }

That's enough for our first implementation.

The important line is:

connection.ensure_connection()

This asks Django to make sure a usable database connection exists.

If the connection can be established, the check succeeds.

If the database is unavailable, authentication fails, the host cannot be reached, or another database-level exception occurs, the check returns an error.


Why Not Execute a Query?

We could write something like:

from django.db import connection


def check_database():
    try:
        with connection.cursor() as cursor:
            cursor.execute("SELECT 1")

        return {"status": "ok"}

    except Exception as exc:
        return {
            "status": "error",
            "error": str(exc),
        }

This is also a valid approach.

However, for the first version of django-healthkit, ensure_connection() is enough to verify that Django can establish the database connection.

Keeping the check minimal also means we don't need to make assumptions about the database backend.

Later, we can decide whether a real query should be part of the database health check.


Cache Health Check

The second dependency we want to monitor is the cache.

Django's cache framework supports multiple backends, including Redis, Memcached, database cache, file-based cache, and local-memory cache.

This is important because our health check shouldn't care which backend the application is using.

It should simply ask Django:

Can I write to the cache and read the value back?

Create:

src/django_healthkit/checks/cache.py

with:

from django.core.cache import cache


def check_cache():
    key = "django_healthkit:health_check"
    value = "ok"

    try:
        cache.set(key, value, timeout=10)

        if cache.get(key) != value:
            return {
                "status": "error",
                "error": "Cache value could not be retrieved.",
            }

        return {
            "status": "ok",
        }

    except Exception as exc:
        return {
            "status": "error",
            "error": str(exc),
        }

    finally:
        cache.delete(key)

Here we're doing three things:

  1. Write a temporary value.
  2. Read it back.
  3. Delete it afterwards.

The important part is:

cache.set(key, value, timeout=10)

followed by:

cache.get(key)

If the cache backend is unavailable, cache.set() or cache.get() can fail.

If the value cannot be retrieved correctly, we also consider the cache unhealthy.


Why Use a Temporary Key?

We don't want the health check to interfere with application data.

That's why we use a dedicated key:

django_healthkit:health_check

and give it a short timeout:

timeout=10

We also remove it in finally:

finally:
    cache.delete(key)

This keeps the health check isolated from the application's normal cache entries.


Testing the Checks

Now that we have implemented the two checks, we should test them.

Create:

tests/
├── __init__.py
└── test_checks.py

Add:

from django_healthkit.checks.cache import check_cache
from django_healthkit.checks.database import check_database


def test_database_check():
    result = check_database()

    assert result["status"] == "ok"


def test_cache_check():
    result = check_cache()

    assert result["status"] == "ok"

Run the tests:

pytest

If everything is configured correctly, we should see both checks passing.


Simulating a Failure

A health check is not very useful if we only test the happy path.

We should also verify that failures are handled correctly.

For example, we can temporarily replace the cache backend with Django's dummy cache:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.dummy.DummyCache",
    }
}

The dummy cache doesn't actually store values.

Therefore:

cache.set(key, value)

followed by:

cache.get(key)

will not return the value we stored.

Our check will correctly report:

{
    "status": "error",
    "error": "Cache value could not be retrieved."
}

Django provides several built-in cache backends, so testing the check independently of a specific backend is important.


Keeping the Checks Independent

At this point, we have two independent functions:

check_database()

and:

check_cache()

Neither function knows anything about the other.

That's intentional.

We don't want something like this:

def health_check():
    # database
    # cache
    # disk
    # memory
    # cpu

to become one giant function.

Instead, each component should have its own check.

This gives us a much cleaner architecture:

health_check
     │
     ├── database
     ├── cache
     ├── disk
     ├── memory
     └── cpu

The main health-check system can then execute these checks and combine their results.


What's Next?

We now have the first two real components of django-healthkit.

The package can already answer two important questions:

  • Is the application able to connect to the database?
  • Is the configured Django cache backend working correctly?

The next step is to build a small health-check manager that can execute multiple checks and return a single structured result.

Eventually, we want something like:

{
    "status": "ok",
    "checks": {
        "database": {
            "status": "ok"
        },
        "cache": {
            "status": "ok"
        }
    }
}

That manager will become the foundation for the health endpoint we'll build later.

For now, we have a good starting point: two small, independent, testable health checks.

In the next part, we'll build the health check manager and connect these individual checks together.

Related Notes

End of note.