Ah, yes, the transaction stuff.  The transaction commits only once the request is returning through the middleware, so the CourseEmail object doesn't really exist in a way that the worker can actually access until that time.   And all it waits for is for the task to be successfully queued, rather than anything about how successfully it runs when it starts running.  The "normal" way that Celery recommends that such models exist is to explicitly handle them in their own transaction, so that the CourseEmail object actually exists before the task is queued.  I'm not sure which is uglier, but there should certainly be a comment put here that explains the retry.