Back to home

Django: Locking database records before updates

Table of Contents

When should you lock database records?

You've got a Django project, and you're writing a piece of code to fetch a record and then change it afterwards, before saving the changes back into the database.

If you've ever written a custom 'update or create' function, you'll have implemented this pattern before.

The key aspect to consider is the selection part. Let's say that you have two possible ways to try to fetch an existing record. You might run some code like this:

# Try fetch object based on ID
obj = MyModel.objects.filter(id=1).first()

# If no object found, try to select it by slug
if not obj:
    obj = MyModel.objects.filter(slug='some-slug').first()

if obj:
    # change some value
    obj.foo = 'bar'
    obj.save(['foo'])

The issue with this approach is that in between the selection and the update/save, another piece of code could potentially make changes to the model you retrieved.

Because my above example is very simple, the code should run and finish almost immediately, meaning that it's very unlikely to encounter this problem. But, for argument's sake, let's say we're updating a lot of fields, and the database we're using is very large, making the queries slower.

It's now very possible that in the time between us selecting the record, altering and then saving it, another piece of code could have amended that same record, causing a conflict and potential loss of data from one of the two processes. That's where locking the record comes into play.

What does it mean to 'lock' the record?

To 'lock the record' means to tell the database that this particular piece of data is currently in use and shouldn't be given to anyone else who requests it. We're telling the database that "until I'm done, nobody else can use this information".

Once your process is complete, the database is told that the record is free to be used again, and it can serve the information to the next process that needs it.

How to lock down a record

For the sake of accuracy, I'm going to provide a code snippet taken directly from a project I've been working on. But, firstly, some context:

This is a QuerySet subclass with a new method called update_or_create_from_remote_taxonomy. This method is given a remote_taxonomy object (a dataclass with some information attached), and this method should then find the appropriate record in the database to match this object, and then should update the record in the database to match the object's data. This functionality is all part of a larger system which is regularly importing data from a remote source and keeping our copy up-to-date.

from django.db import models, transaction

class TaxonomyQuerySet(models.QuerySet):
    def update_or_create_from_remote_taxonomy(
        self, remote_taxonomy
    ) -> tuple["Taxonomy", bool]:
        remote_id = (remote_taxonomy.remote_id or "").strip() or None

        # atomic transaction ensures that the record is locked until the end of the block
        with transaction.atomic():
            taxonomy_obj = None
            if remote_id:
                taxonomy_obj = (
                    self.select_for_update().filter(remote_id=remote_id).first()
                )
            if not taxonomy_obj:
                taxonomy_obj = (
                    self.select_for_update().filter(slug=remote_taxonomy.slug).first()
                )

            if taxonomy_obj:
                data = remote_taxonomy.updating_data()
                for key, value in data.items():
                    setattr(taxonomy_obj, key, value)
                taxonomy_obj.save(update_fields=list(data.keys()))
                return taxonomy_obj, False

            taxonomy_obj = self.create(**remote_taxonomy.creating_data())
            return taxonomy_obj, True

You can see on line 9 that I've added a comment to show where the magic happens. Using transaction.atomic, you can start a with block. Within that block, any records that you fetch using select_for_update will automatically flag in the database as being locked. Once the with block ends, these records are automatically released.

From line 10, you can see that we're trying to select a record based on two criteria, locking the retrieved record (if any) using select_for_update. If we find an object (line 21), we then loop through some data to update the record and save it back to the database. If we don't find any records (line 28), we just create a new one.

Locking records in Wagtail CMS

This same approach can be used in Wagtail to lock records. As Wagtail is built with Django, the process is identical. However, this only applies to standard Django models, and not Wagtail's page models. Wagtail's pages have their own system for locking records between edits. So if you're using Wagtail, you can follow this guide like normal for any Django models, such as what you'd use in snippets or taxonomies. But you shouldn't apply this to instances of page models.

FAQs

How do you lock a record in a Django database?

Using Model.objects.select_for_update, you can retrieve a record from the database, locking it from being edited by other processes. Using this in combination with django.db.transaction.atomic, you can efficiently lock and release records to preserve data integrity.

What happens if another process tries to reteive a locked record?

If another process tries to use a record that you have locked, it'll simply hang until the record is free. This has a certain risk that a request could timeout if the record isn't freed up within a certain window. However, you should be intentional with how and when you lock your records to ensure this doesn't happen. In general, locking records shouldn't result in any errors, as processes will naturally hang until they're served the data they need.

Why should you lock records in Django?

If you're fetching and then updating records in separate transactions, there is the possibility that another process could also select/update that record in the database in the meantime. If these two processes then make changes and save them back to the database, their work could be overwritten by the other, resulting in unexpected data loss. Locking records ensures better reliability and data integrity.

Related blogs

  • django-testing-patterns
    Useful Django unit test patterns

    This is a collection of some common unit test patterns I find myself re-implementing across various projects. To save myself (and maybe some others) time in the future, I've decided to collect them all in one place.

    20 March 2026
  • writing-wagtail-streamfield-content-migrations
    Writing Wagtail StreamField content migrations

    When you drastically change one of your StreamFields and generate a migration for your new structure, the migration won't actually update what's already in your database. It'll describe the new, intended structure behind the fields, but the JSON data itself will remain exactly as it is.

    18 February 2026
  • directly-filtering-wagtail-pages
    Directly filtering Wagtail parent/child pages

    In Wagtail, pages don't have a typical, direct relationship like you'd expect in Django. Instead of using foreign keys to link them, each page has a `path` and a `depth` attribute...

    24 November 2025