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

The API Worked. The Architecture Didn’t.

A practical look at why a working API does not always mean a healthy architecture, covering coupling, synchronous work, database bottlenecks, caching, microservices, failure modes, and the importance of clear boundaries.

When Redis Is Not the Solution

Redis is fast and extremely useful, but that doesn't make it the right solution for every backend problem. A practical look at when Redis helps with caching, rate limiting, OTPs, locks, queues, sessions, and temporary data—and when it simply adds unnecessary infrastructure and complexity.

Django Transactions: What atomic() Actually Protects

Learn how Django transactions work, what transaction.atomic() actually protects, and how to handle race conditions, row locking, database constraints, nested transactions, and post-commit side effects with select_for_update() and on_commit().

The Hidden Cost of Django REST Framework Serializers

Learn how Django REST Framework serializers can cause N+1 queries, slow API responses, and unnecessary database work—and how to optimize them with select_related, prefetch_related, annotations, and better serializer design.

Building a Django Package — Part 4: Publishing django-healthkit to PyPI

In this final part, we prepare django-healthkit for release, build and validate the package, test it on TestPyPI, publish it to PyPI, and create a Git tag and GitHub release.

Building a Django Package — Part 3: Health Check Manager and Endpoint

In Part 3, we connect the database and cache health checks, build the health check manager, expose a Django health endpoint, measure check latency, and return a structured health status.

Building a Django Package — Part 1: Setting Up django-healthkit

In this part, we build the initial structure of django-healthkit, configure the package with pyproject.toml, and prepare it for development.

Building a Secure Webhook Receiver for Server-to-Server Communication

How I built a secure FastAPI webhook receiver using RSA signatures to enable authenticated server-to-server communication, proxy requests, and connect applications across different network environments.

Why I Switched to Conventional Git Commit Messages

Why I switched from inconsistent Git commit messages to a Conventional Commits style, with practical examples for cleaner and more maintainable Git history.

Building django-healthkit

How a simple health endpoint for Bidar turned into django-healthkit, a lightweight Django health-check package.

Designing a Gold Jewelry E-Commerce Database with Django

Learn how to design a scalable Django database for a gold jewelry e-commerce platform by modeling products, attributes, and purchasable product variants using real-world domain-driven design principles.

How to Fix Common Next.js 16 Build Errors (Proxy, Dynamic Rendering & Revalidation)

Learn how to fix common Next.js 16 build errors, understand Proxy, Dynamic Rendering, and Revalidation, and improve your application's SEO and performance.

Why I Built cyber-ui: A Minimal Design System for Developers

How my projects, from backend systems to monitoring tools, led me to create a minimal and personal UI foundation.

End of note.