Task Bucket
self.task_bucket is available on every App instance. It runs background work and offloads blocking calls to threads. The task bucket tracks all spawned tasks and cancels them on shutdown.
Spawning Background Tasks
self.task_bucket.spawn(coro, *, name=None) creates a tracked background task from a coroutine. The task bucket owns the task's lifecycle. The returned asyncio.Task is available for inspection or cancellation.
# Fire off a background coroutine — the bucket tracks and cancels it on shutdown
self.task_bucket.spawn(self.poll_sensor(), name="poll_sensor")
The polling loop runs indefinitely without blocking the handler that started it. On shutdown, the bucket cancels it.
Offloading Blocking Code
run_in_thread(fn, *args, **kwargs) runs a synchronous function in a thread pool. The event loop stays unblocked while the thread works. The return value is a coroutine that resolves to the function's result.
# Run a blocking call without freezing the event loop
data = await self.task_bucket.run_in_thread(self.expensive_sync_call)
self.logger.info("Got: %s", data)
run_in_thread suits HTTP clients without async support, database drivers, file I/O, and CPU-bound computation.
Normalizing Sync/Async Callables
Advanced: make_async_adapter
make_async_adapter(fn) wraps any callable, sync or async, into a consistent async callable. Sync functions route through run_in_thread() automatically.
# Normalize a sync-or-async callable into an async callable
handler = self.task_bucket.make_async_adapter(self.maybe_sync_handler)
await handler() # always safe to await regardless of original type
Apps that wrap third-party integrations often receive callables of unknown type — a config-provided callback, a plugin hook, or a library method that may or may not be async. The adapter normalizes them into one interface.
Cross-Thread Communication
Advanced: cross-thread primitives
Four methods handle the narrow case where code in one thread needs to reach into another. Typical automations rarely need them.
Posting to the Event Loop
post_to_loop(fn, *args, **kwargs) schedules a callable on the main event loop from any thread. The call is non-blocking. It queues the work and returns immediately.
# Schedule a callback on the event loop from any thread
self.task_bucket.post_to_loop(self.on_data_ready, "sensor.temperature")
Running Async from Sync Code
run_sync(coro) submits a coroutine to the event loop and blocks the calling thread until it completes. It accepts a coroutine object, not a callable.
# Inside a thread (run_in_thread or AppSync), call async code with run_sync
state = self.task_bucket.run_sync(self.api.get_state("sensor.temperature"))
Warning
run_sync() blocks the calling thread. Calling it from the event loop thread causes a deadlock. It is safe inside run_in_thread() callbacks and AppSync lifecycle methods only.
Running on the Loop Thread
run_on_loop_thread(fn, *args, **kwargs) runs a synchronous function on the main event loop thread. Loop-affine code that must not run in a worker thread belongs here.
Creating Tasks from Any Context
create_task_on_loop(coro, *, name=None) creates a task on the event loop from any thread context. The bucket tracks it like any other spawned task.
Shutdown
The bucket cancels all tracked tasks when the app shuts down. Hassette cancels every pending task, waits up to task_cancellation_timeout_seconds (configurable in global settings) for them to finish, and logs warnings for any tasks that do not exit within the timeout.
Manual cleanup is not required.
See Also
- Apps Overview for core capabilities and common patterns
- Lifecycle for when shutdown happens and in what order
- App Cache for persisting data across restarts (the task bucket is for in-memory work only)