Djangocon EU: reliable Django signals - Haki Benita

Tags: django, djangocon

(One of my summaries of the 2026 Djangocon EU in Athens).

He’s going to talk about:

  • A pattern for decoupling modules in complicated systems

  • Reliable and fault-tolerant workflows in your elaborate Django apps. (A workflow is a set of actions, like “select product”, “place order”, “pay”).

For the payment example, you probably use a third party. You create a payment in the third party payment processor. The user provides details at the processor. After some time, the payment processor sends back some completion information. The completion info can contain “succeeded” or “failed”.

Handling the order is a bit more complicated. You have the order itself with its own workflow (“placed”, “payment_succeeded”, “sent”, “received”). But this workflow also depends on the payment workflow.

So two processes intertwined. Both have a webhook (“order placed” and “third party processor finished”). When the payment processor is finished, the webhook for the payment cannot normally talk to the Order model. If Order needs to talk to Payment and Payment to Order, you get circular dependencies. You can work around it, but this example only has a two-way relation. In a real order+payment example you also have refunds, customers and lots more.

You get spaghetti code. Dependencies should only flow one way. How do you solve it? You could start polling for changes every hour. Or every 10 seconds. It is reliable, but it either gives a lot of unnecessary load on the system or it gives lots of delays.

There’s a better solution: signals. Django has it build-in. You can send a specific signal upon payment processing. And other pieces of the code can subscribe to the signal. So you get signalled (…) immediately and you don’t have hard dependencies: hurray!

But… did we really achieve good signals? There are some problems:

  • When the receiver fails (with some exception), the sender also fails. You can use .send_robust() instead of .send(), it doesn’t fail upon receiver exceptions. This already helps a lot. But in that case the state of the system might not be right. The customer paid, but won’t get the order.

  • What about database transactions? If the reciever causes a database error, making the sender’s transaction abort? Bad. Not really decoupled. And what about side-effects? What if I am sending a “success” email inside the transaction and then the transaction gets aborted?

Fault tolerance is important. What do we want:

  • Keep models decoupled.

  • Recievers should have minimal effect on the sender.

  • Guarantee at-least-once delivery.

  • Mechanism for handling failed recievers.

We could use a database queue to handle signals. Signals are then inside the database transaction, which means receivers are not processed immediately.

Django added a tasks framework. in version 6.0. It is an interface, not an actual implementation: you can plug in your own implementations. There’s a django db task queue that handles the queue stuff inside the databases.

In the task message, you can pass a “dotted name” for a function (some.module:some_function) and pkg_util helps you to import the actual function afterwards. Pass it some generic arguments in the task.

In your signals, you can now enqueue a task instead of calling a function directly.

  • Everything is decoupled: a dotted path string is used.

  • Minimal effect: it runs in a separate process.

  • At-least-once delivery: database transactions ensure the task is enqueued.

  • Mechanism for handling failed recievers: the django tasks framework ensures failing tasks are known and can be re-tried.

For more information and full code examples, see his own blog: https://hakibenita.com/django-reliable-signals

https://reinout.vanrees.org/images/2026/ottbergen5.jpeg

Unrelated photo explanation: a recent trip to the “Modellbundesbahn” in Germany. The old, big signal box in Ottbergen, where the line splits towards Nordheim and Kreiensen.