Type something to search...
Using MongoDB with Django: Options and Best Practices

Using MongoDB with Django: Options and Best Practices

Django's ORM is one of the best things about the framework. Models drive the admin, forms, authentication, serializers in Django REST Framework, and dozens of third-party apps. The trouble is that the ORM was designed for relational databases, and for most of Django's history, using MongoDB meant giving up much of that ecosystem or relying on community projects that fell out of maintenance.

That's changed. MongoDB now maintains an official Django database backend, which plugs MongoDB into the ORM itself. Alongside it, you still have the older approaches: an ODM like MongoEngine, or talking to MongoDB directly with PyMongo while keeping a relational database for everything else. Each is the right answer for a different kind of project.

This guide covers all three options, how to set each one up, what you gain and give up, and the practices that keep a Django plus MongoDB project healthy regardless of which path you choose.

The Options at a Glance

ApproachUses Django ORMAdmin, auth, formsBest for
Django MongoDB Backend (official)YesYes, with some limitsNew projects that want MongoDB as the primary database
MongoEngineNo, its own ODMNo (not natively)APIs that want a document-first model and don't need the admin
PyMongo alongside a SQL databaseNoYes, via the SQL databaseAdding MongoDB for specific data to an existing Django app
DjongoPartiallyPartiallyAvoid for new work; not actively maintained

The rest of this post walks through each in order.

Option 1: The Official Django MongoDB Backend

The django-mongodb-backend package is a Django database backend maintained by MongoDB. You configure it in DATABASES like PostgreSQL or SQLite, and your models, querysets, and many built-in apps work against MongoDB. Its version numbers track Django's (for example, the 5.2.x series targets Django 5.2), so install the release that matches your Django version.

Setup

The easiest start is MongoDB's project template, which pre-configures the pieces that differ from a default Django project:

python -m venv .venv
source .venv/bin/activate
pip install django-mongodb-backend

django-admin startproject mysite --template https://github.com/mongodb-labs/django-mongodb-project/archive/refs/heads/5.2.x.zip

The template's branch name should match your Django version. The database configuration looks like this:

# mysite/settings.py
import os

DATABASES = {
    "default": {
        "ENGINE": "django_mongodb_backend",
        "HOST": os.environ["MONGODB_URI"],  # e.g. mongodb+srv://user:pass@cluster0.example.mongodb.net/
        "NAME": "mysite",
    },
}

DEFAULT_AUTO_FIELD = "django_mongodb_backend.fields.ObjectIdAutoField"

Earlier releases used a django_mongodb_backend.parse_uri() helper to build this dictionary from a connection string. If you see it in older examples, it does the same job; check the docs for your version to see which form is current.

The DEFAULT_AUTO_FIELD setting is the important difference from a relational project. Primary keys are ObjectIds rather than auto-incrementing integers. The template also sets up app configs and migration modules for Django's contrib apps (admin, auth, contenttypes) so their models use ObjectIdAutoField too. If you add MongoDB to an existing project by hand rather than from the template, that's the part to get right.

Defining Models

Regular Django models work as you'd expect:

# catalog/models.py
from django.db import models
from django_mongodb_backend.fields import ArrayField, EmbeddedModelField
from django_mongodb_backend.models import EmbeddedModel


class Dimensions(EmbeddedModel):
    width_cm = models.FloatField()
    height_cm = models.FloatField()
    depth_cm = models.FloatField()


class Product(models.Model):
    sku = models.CharField(max_length=32, unique=True)
    name = models.CharField(max_length=200)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    tags = ArrayField(models.CharField(max_length=40), default=list, blank=True)
    dimensions = EmbeddedModelField(Dimensions, null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table = "products"
        indexes = [models.Index(fields=["name"])]

    def __str__(self):
        return self.name

This is where the backend becomes more than "SQL Django with a different engine". EmbeddedModelField stores Dimensions as a subdocument inside each product, and ArrayField stores tags as a native array. Neither needs a join table or a separate collection. The stored document looks like:

{
  "_id": { "$oid": "66f2d9a3c1e4b0a7d3f10b21" },
  "sku": "LAMP-001",
  "name": "Brass Desk Lamp",
  "price": { "$numberDecimal": "24.50" },
  "tags": ["lighting", "office"],
  "dimensions": { "width_cm": 18, "height_cm": 45, "depth_cm": 18 },
  "created_at": { "$date": "2026-09-14T08:43:00Z" }
}

Run python manage.py makemigrations and migrate as usual. With MongoDB there are no tables to alter, so migrations mainly create collections, indexes, and unique constraints. That's still worth doing: the unique=True on sku becomes a real unique index only because the migration creates it.

Querying

Querysets work, including lookups into embedded fields:

from catalog.models import Product

# Standard filters
Product.objects.filter(price__lte=50, tags__contains=["office"]).order_by("-created_at")[:20]

# Filter on an embedded document field
Product.objects.filter(dimensions__height_cm__gt=40)

# Aggregates
from django.db.models import Avg, Count
Product.objects.aggregate(avg_price=Avg("price"), total=Count("id"))

Under the hood, the backend compiles each queryset into a MongoDB aggregation pipeline. When you need something the ORM can't express, such as $unwind, $facet, or Atlas Search stages, use a MongoManager and raw_aggregate():

from django_mongodb_backend.managers import MongoManager


class Product(models.Model):
    # ... fields as above ...
    objects = MongoManager()


top_tags = Product.objects.raw_aggregate([
    {"$unwind": "$tags"},
    {"$group": {"_id": "$tags", "count": {"$sum": 1}}},
    {"$sort": {"count": -1}},
    {"$limit": 5},
])

raw_aggregate() returns model instances where the pipeline output has the fields to build them. For grouped results like this one, where the output shape differs from the model, it's often clearer to use PyMongo directly on the collection, which the next section covers.

Limitations to Know

The official backend is a big step forward, but it's still mapping a relational ORM onto a document database. Keep these in mind:

  • Relations become $lookups. ForeignKey and select_related work, but each join is a $lookup stage. Heavily relational schemas lose most of MongoDB's advantages. Model with embedding where data is read together.
  • Not every ORM feature is supported. Some database functions, certain aggregation and subquery patterns, and some third-party apps that assume SQL behavior may not work. The backend's docs list known limitations; check them before you depend on a particular feature.
  • Transactions and atomic() have had limited support compared to relational backends. If your code relies on transaction.atomic() for correctness, verify how your version behaves before shipping.
  • Third-party apps that ship migrations with integer primary keys may need adjustments, since MongoDB projects use ObjectIdAutoField.

For a new project where MongoDB is the primary database and you want the admin, auth, and the broader Django ecosystem, this backend is now the default recommendation.

Option 2: MongoEngine

MongoEngine is a mature, long-standing ODM (object-document mapper) for Python. It doesn't plug into the Django ORM; it replaces it with its own document classes. It works fine inside a Django project, but Django's admin, ModelForms, and auth models won't use it.

pip install mongoengine
# mysite/settings.py
import os
import mongoengine

mongoengine.connect(host=os.environ["MONGODB_URI"], db="mysite")
# catalog/documents.py
from datetime import datetime, timezone

from mongoengine import (
    DateTimeField, DecimalField, Document, EmbeddedDocument,
    EmbeddedDocumentField, FloatField, ListField, StringField,
)


class Dimensions(EmbeddedDocument):
    width_cm = FloatField()
    height_cm = FloatField()


class Product(Document):
    sku = StringField(required=True, unique=True, max_length=32)
    name = StringField(required=True, max_length=200)
    price = DecimalField(precision=2, force_string=False)
    tags = ListField(StringField(max_length=40))
    dimensions = EmbeddedDocumentField(Dimensions)
    created_at = DateTimeField(default=lambda: datetime.now(timezone.utc))

    meta = {"collection": "products", "indexes": ["name", ("tags", "-created_at")]}

Queries use a similar double-underscore syntax:

Product.objects(tags="office", price__lte=50).order_by("-created_at").limit(20)
Product.objects(sku="LAMP-001").update_one(inc__stock=-1)

MongoEngine is a good fit for API-only Django projects (for example, a DRF backend where you write your own serializers) that want document-first modeling and don't care about the admin. Its drawbacks: it's a community project, so check that its release cadence matches your needs, and DecimalField stores values as floats or strings rather than Decimal128 by default, so money needs care. It also adds another abstraction layer between you and PyMongo, which can make performance tuning less transparent.

Option 3: PyMongo Alongside a Relational Database

Many real projects don't need MongoDB for everything. They keep PostgreSQL for users, orders, and the admin, and add MongoDB for a specific workload: event logs, product catalogs with wildly varying attributes, user-generated content, or analytics. In that case, the simplest approach is often the best one: use PyMongo directly for the MongoDB part.

Create one client per process in a small module:

# core/mongo.py
from functools import lru_cache

from django.conf import settings
from pymongo import MongoClient


@lru_cache(maxsize=1)
def get_client() -> MongoClient:
    return MongoClient(settings.MONGODB_URI, appname="mysite", maxPoolSize=50)


def get_db():
    return get_client()[settings.MONGODB_DB]

Use it from views, services, or Celery tasks:

# activity/views.py
from datetime import datetime, timezone

from django.http import JsonResponse
from django.views.decorators.http import require_POST

from core.mongo import get_db


@require_POST
def track_event(request):
    get_db().events.insert_one({
        "userId": request.user.pk,
        "type": request.POST["type"],
        "path": request.POST.get("path"),
        "ts": datetime.now(timezone.utc),
    })
    return JsonResponse({"ok": True}, status=201)


def recent_activity(request):
    cursor = (
        get_db().events
        .find({"userId": request.user.pk}, {"_id": 0, "type": 1, "path": 1, "ts": 1})
        .sort("ts", -1)
        .limit(25)
    )
    return JsonResponse({"events": list(cursor)}, json_dumps_params={"default": str})

You reference relational rows by primary key (userId here) rather than with foreign keys. There's no cross-database join, so design the MongoDB documents to hold whatever they need to render on their own.

The lazy lru_cache matters with servers like Gunicorn that fork worker processes. A MongoClient created before the fork is shared across children, which PyMongo warns about because the connection pool and monitoring threads aren't fork-safe. Creating the client on first use means each worker builds its own after forking. For more on the driver itself, see Connecting Python Apps to MongoDB with PyMongo.

If you use async Django views, use PyMongo's AsyncMongoClient in those code paths instead of the synchronous client, so queries don't block the event loop.

What About Djongo?

Djongo translated Django's SQL into MongoDB queries and was the popular choice for years. It has not been actively maintained for a long time, it lags behind current Django versions, and its SQL-translation approach has known correctness gaps. Avoid it for new projects, and if you have a Djongo project, the official backend is the natural migration target.

Best Practices, Whichever Path You Choose

Model for your reads, not for normalization. Whether you use the ORM backend or PyMongo, the biggest wins come from embedding data that's read together. A Django habit of splitting everything into related models and joining at query time carries the relational cost without the relational engine.

Create one client per process. The official backend manages this for you. With PyMongo or MongoEngine, connect once at startup (or lazily per process), never per request. Multiply your pool size by the number of Gunicorn or uWSGI workers when estimating connections against your cluster's limit.

Make indexes part of deployment. With the backend, that's migrations. With MongoEngine, meta["indexes"] is applied lazily and can be triggered with ensure_indexes(). With PyMongo, write a management command that calls create_index and run it in your deploy pipeline.

Validate at the boundary. MongoDB won't reject a document with a misspelled field unless you add schema validation. Django forms, DRF serializers, or Pydantic models should validate input before it reaches the database. For defense in depth, add schema validation with JSON Schema on important collections.

Store money as Decimal128. Django's DecimalField on the official backend maps to Decimal128. With PyMongo, convert decimal.Decimal values with bson.Decimal128. Avoid floats for currency on every path.

Use timezone-aware datetimes. Keep USE_TZ = True, and store UTC. MongoDB dates carry no time zone, and PyMongo returns naive datetimes unless you pass tz_aware=True to the client.

Test against a real MongoDB. Run the official mongo Docker image in CI rather than mocking the database. Django's test runner creates and destroys a test database with the official backend just as it does for PostgreSQL.

Choosing Between Them

A quick decision guide:

  • Starting fresh, MongoDB is the main database, and you want the admin and auth: use the official Django MongoDB Backend.
  • Existing Django app on PostgreSQL, adding MongoDB for one or two workloads: keep the ORM on PostgreSQL and use PyMongo for the MongoDB data.
  • API-only project, document-first design, and you're comfortable writing your own serializers: MongoEngine or PyMongo both work; PyMongo keeps you closer to the database with fewer layers.
  • On Djongo today: plan a move to the official backend.

Conclusion

Using MongoDB with Django is no longer a compromise. The official backend brings MongoDB into the ORM with embedded models and arrays as first-class fields, while PyMongo remains a clean way to add MongoDB to an existing relational project, and MongoEngine still serves document-first APIs. The best choice depends on whether you need the Django ecosystem on top of MongoDB or just MongoDB next to it.

As a next step, spin up a throwaway project from the official template, point it at a free Atlas M0 cluster, and define one model with an EmbeddedModelField. Ten minutes with the admin and the Django shell will tell you more about whether the backend fits your project than any comparison table.

Tags :
Share :

Related Posts

A Complete Guide to MongoDB Query Operators

A Complete Guide to MongoDB Query Operators

Your first MongoDB queries are usually simple equality filters: find the user with this email, find orders with this status. That covers a surprising

Continue Reading
Async MongoDB in Python with Motor and FastAPI

Async MongoDB in Python with Motor and FastAPI

FastAPI runs your endpoints on an event loop. That's what lets a single worker juggle hundreds of concurrent requests: while one request waits on the

Continue Reading
Atlas Online Archive: Tiering Cold Data to Cut Costs

Atlas Online Archive: Tiering Cold Data to Cut Costs

Look at almost any production database and you'll find the same shape. A small slice of recent data gets nearly all the reads and writes: this week's

Continue Reading