Django: Using get_or_create to prevent race conditions

Adrienne Domingus
3 min readFeb 2, 2018

get_or_create, is an awesome helper utility to have at your disposal when you need an object matching some specifications, but there should only be exactly one match — you want to retrieve it if it already exists, and create it if it doesn’t.

However, there’s a scenario where it doesn’t quite do what we expect in the case of race conditions (exactly the thing we’re trying to prevent). There is a nod to what we’re about to talk about in the docs:

This method is atomic assuming correct usage, correct database configuration, and correct behavior of the underlying database. However, if uniqueness is not enforced at the database level for the kwargs used in a get_or_create call (see unique orunique_together), this method is prone to a race-condition which can result in multiple rows with the same parameters being inserted simultaneously.

Let’s talk about this in more detail. Here’s the relevant bit of Dango’s implementation of get_or_create:

lookup, params = self._extract_model_params(defaults, **kwargs)
return self.get(**lookup), False
except self.model.DoesNotExist:
return self._create_object_from_params(lookup, params)

This does exactly what the name implies! It attempts to do a lookup based on the filter args that are passed in (explicitly doing a .get(), which fails with DoesNotExist if there is no match in the database), and then catching that DoesNotExist exception, and creates the object instead.

However, if you go further into _create_object_from_params, you’ll notice that it does a lot more than just make a call to .create(). Here’s what happens there (still in Django source code):

with transaction.atomic(using=self.db):
obj = self.create(**params)
return obj, True
except IntegrityError:
exc_info = sys.exc_info()
return self.get(**lookup), False
except self.model.DoesNotExist:

This is cool — it’s explicitly accounting for race conditions! It tries to create the object, but if that operation throws an IntegrityError, it does the lookup again and tries to return what it finds.