The Configuration Lifecycle
Celery uses a lazy configuration strategy to provide flexibility during application startup. This allows developers to define settings, load them from objects, or modify them directly on the application instance before the configuration is "locked in" for use by workers or tasks.
The lifecycle transitions through three distinct states: Initialization, Pending, and Finalized.
Initialization
When a Celery instance is created in celery/app/base.py, it initializes its configuration attribute (self._conf) as a Settings object. However, this Settings object is initially backed by a PendingConfiguration proxy rather than a concrete dictionary of values.
# celery/app/base.py - Celery.__init__
self._conf = Settings(
PendingConfiguration(
self._preconf, self._finalize_pending_conf),
prefix=self.namespace,
keys=(_old_key_to_new, _new_key_to_old),
)
At this stage:
self._preconfstores any settings passed directly to theCeleryconstructor (likebrokerorbackend).self._finalize_pending_confis registered as the callback to trigger the transition to the finalized state.
The Pending Phase
The PendingConfiguration class (found in celery/app/base.py) acts as a buffer. It allows you to set configuration values directly on app.conf without triggering the full loading process (which might involve expensive imports or environment lookups).
While in this phase, app.configured remains False. You can update settings using standard dictionary methods or attribute access:
app = Celery(broker='amqp://')
app.conf.task_always_eager = True # Stored in PendingConfiguration._data
app.conf.update(worker_prefetch_multiplier=10)
print(app.configured) # False
The PendingConfiguration stores these changes in its internal _data dictionary (which is a reference to the app's _preconf). It remains in this state until a value is read from the configuration.
The Finalization Process
Finalization is the transition from a proxy to a concrete Settings object. It is triggered automatically the first time any key is accessed on app.conf.
Triggering Finalization
The PendingConfiguration class implements a data property decorated with @cached_property. Accessing any key via __getitem__ or iterating over the config triggers this property, which in turn executes the callback (the app's _finalize_pending_conf method).
# celery/app/base.py - PendingConfiguration
@cached_property
def data(self):
return self.callback()
The _load_config Sequence
When finalization is triggered, the Celery._load_config method performs the following steps:
- Signal Dispatch: Sends the
on_configuresignal. - Source Loading: If
app.config_from_object()was called previously, the loader now imports and reads that object. - Setting Detection: Merges the loaded configuration with the
_preconfvalues. - Default Application: Iterates through
_pending_defaults(promises added viaapp.add_defaults()) and applies them. - State Update: Sets
self.configured = True. - Post-Configure Signal: Sends the
on_after_configuresignal with the finalized settings.
The Finalized State
Once finalized, app.conf is a fully populated Settings object (defined in celery/app/utils.py). This class inherits from ConfigurationView, which provides a unified view over multiple layers of configuration:
- Changes: User-provided overrides.
- Defaults: The standard Celery default settings.
Key Mapping and Compatibility
The Settings object handles the translation between old-style (uppercase, e.g., CELERY_BROKER_URL) and new-style (lowercase, e.g., broker_url) setting names. It uses the _old_key_to_new and _new_key_to_old mapping functions passed during initialization.
It also provides specialized properties for critical settings that check environment variables before falling back to the configuration:
# celery/app/utils.py - Settings
@property
def broker_url(self):
return (
os.environ.get('CELERY_BROKER_URL') or
self.first('broker_url', 'broker_host')
)
Important Considerations
Early Finalization "Gotcha"
Because finalization is triggered by key access, inspecting app.conf too early can lock the configuration before you have finished setting it up.
app = Celery()
print(app.conf.broker_url) # Triggers finalization with defaults
app.config_from_object('myconfig') # May require force=True now
If you need to load a configuration object after the app has already been finalized, you must use the force=True parameter in config_from_object.
Task Auto-Finalization
Accessing the app.tasks registry also triggers a broader app finalization via app.finalize(auto=True). This ensures that all tasks are bound to the app and the configuration is stable before any task execution logic is invoked.
# celery/app/base.py - Celery.tasks
@cached_property
def tasks(self):
self.finalize(auto=True)
return self._tasks