How the Music Assistant Task Manager Handles Background Tasks

The Music Assistant server uses a dedicated TasksController in music_assistant/controllers/tasks/controller.py to schedule, queue, and monitor asynchronous background tasks with built-in concurrency limits, persistence, and progress tracking.

The Music Assistant task manager background tasks system provides the infrastructure for long-running operations like library scans and metadata refreshes without blocking the main event loop. Located in the music_assistant.controllers.tasks package within the music-assistant/server repository, this subsystem manages everything from ad-hoc job queuing to recurring scheduled maintenance.

Core Architecture – The TasksController

The TasksController class in music_assistant/controllers/tasks/controller.py serves as the central nervous system for all background operations. It inherits from CoreController, giving it access to the main MusicAssistant instance and standard lifecycle hooks.

Task Registration and Storage

The controller distinguishes between two primary task types:

  • Ad-hoc tasks – Created via run_background_task() for one-time operations like user-initiated playlist refreshes
  • Recurring tasks – Registered via register_scheduled_task() for periodic maintenance like daily library cleanup

Both methods create a BackgroundTask model (from music_assistant_models.background_task) and wrap it in a ManagedTask container defined in models.py. The controller stores these in self._tasks, a dictionary mapping task IDs to ManagedTask instances.

Queueing and Execution Logic

Tasks enter a deque called self._pending_task_ids where priority tasks (flagged with priority=True) are inserted at the front. The controller respects self._max_concurrent_tasks, defaulting to 2 concurrent executions as defined in constants.py.

When the worker loop picks a task ID, it creates an asyncio.Task to run the provider-supplied coroutine. During execution, the ACTIVE_TASK_ID context variable (defined in constants.py) is set so that nested calls can locate the current task via TaskExecutionContext from context.py.

Scheduling and Persistence

Recurring tasks integrate with the server's timer subsystem using self.mass.schedule_timer and self.mass.cancel_timer. The next-run timestamp is stored in BackgroundTask.schedule.next_run and persisted under the config key TASK_STATE_CONFIG_KEY ("scheduled_task_states").

On shutdown, the controller unregisters all tasks, removes the TaskLogHandler, and clears persisted schedule state to ensure clean restarts.

Supporting Infrastructure

Constants and Configuration

The constants.py file defines critical operational limits:

  • DEFAULT_TASK_LOG_LINES (250) – Maximum log entries retained per task
  • MAX_FINISHED_TASK_HISTORY (100) – Cap on completed non-recurring tasks
  • TASK_LIFECYCLE_UPDATE_DEBOUNCE (0.25 seconds) – Debounce period for UI updates

Execution Context and Logging

The helpers.py module provides TaskLogHandler, a custom logging handler that intercepts output from any Python logging call made within a task. Log lines are trimmed to the 250-line limit and stored in the task's logs list. The update_task_progress method allows providers to report completion percentages and status messages.

State Serialization

Helper functions serialize_task_state and restore_task_state in helpers.py handle JSON encoding of schedule states, enabling task persistence across server restarts.

Implementing Background Tasks

Providers and controllers interact with the task manager through straightforward async APIs.

Registering a Recurring Task


# From a provider or core controller

tasks_controller.register_scheduled_task(
    task_id="library_cleanup",
    name="Library cleanup",
    handler=cleanup_library,                     # async callable

    schedule=TaskSchedule(interval=86_400),      # every 24 hours

    translation_key="background_task.library_cleanup",
)

Running an Ad-Hoc Task


# Queue immediate execution with priority

await tasks_controller.run_background_task(
    name="Refresh playlist",
    handler=refresh_playlist,                    # async callable

    translation_key="background_task.refresh_playlist",
    user_id=current_user.user_id,
    priority=True,                               # insert at front of queue

)

Querying via WebSocket API

Clients can retrieve visible tasks through the WebSocket API:

await websocket.send_json({"command": "tasks/list"})
response = await websocket.receive_json()
for task in response["result"]:
    print(f"{task['name']} – {task['status']} (progress {task.get('progress', 0)}%)")

API Integration and Security

The TasksController exposes several @api_command decorated methods including tasks/list, tasks/get, tasks/run, and tasks/cancel. The permission model uses UserRole enforcement—only administrators can execute, retry, or cancel arbitrary tasks.

Non-admin users are restricted to viewing tasks they started via list_tasks_for_user and _get_visible_managed_task, which validate task_info.user_id against the requesting user.

Summary

  • The TasksController in music_assistant/controllers/tasks/controller.py manages the full lifecycle of background tasks with a default concurrency limit of 2.
  • Task persistence utilizes the config key "scheduled_task_states" to survive server restarts.
  • Thread safety is enforced by checking get_ident() against self.mass.loop_thread_id and rescheduling via call_soon_threadsafe.
  • Progress tracking combines update_task_progress calls with automatic log capture via TaskLogHandler.
  • API security restricts sensitive operations to admin users while allowing non-admins to view their own tasks.

Frequently Asked Questions

How does Music Assistant limit concurrent background tasks?

The TasksController enforces a concurrency cap of 2 by default through self._max_concurrent_tasks defined in constants.py. The worker loop only creates new asyncio.Task instances when the number of active tasks falls below this limit, queueing additional task IDs in self._pending_task_ids until capacity becomes available.

Where are scheduled task states stored in Music Assistant?

Scheduled task states are serialized using serialize_task_schedule_state from helpers.py and stored under the configuration key "scheduled_task_states" (TASK_STATE_CONFIG_KEY). This JSON-encoded state persists the next_run timestamps and schedule parameters, allowing recurring tasks to resume their cadence after server restarts.

How can providers update task progress?

Providers call self.tasks.update_task_progress(progress, text) where progress is an integer percentage and text is a descriptive message. These updates are automatically debounced by 0.25 seconds (TASK_LIFECYCLE_UPDATE_DEBOUNCE) to prevent UI flooding. Additionally, any standard Python logging output from the task is captured by TaskLogHandler and stored in the task's logs list (capped at 250 lines).

What happens to finished tasks in the Music Assistant task manager?

Completed non-recurring tasks are retained in a history buffer capped at 100 entries (MAX_FINISHED_TASK_HISTORY). Administrators can manually clear this history via the tasks/clear_finished API command. On controller shutdown, all tasks are unregistered and log handlers removed to ensure a clean state for the next server launch.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →