Skip to content

Admin UI

The TaskQ admin UI is a read-only-by-default observability dashboard built with FastAPI and Jinja2. It shows live job, queue, worker, schedule, rate-limit, and reservation state drawn from Postgres. CSRF-protected write operations are available for job cancellation, job retry, and cron schedule management (enable, disable, skip, run-now), but are gated by TASKQ_ADMIN_ACTIONS_ENABLED (default false — set to true to enable them). The rate-limit reset endpoint is additionally gated by TASKQ_ADMIN_UI_ALLOW_RATE_LIMIT_RESET=true.

The frontend uses Alpine.js for reactive components, HTMX for partial-page updates, and Jinja2 partial templates for composable UI pieces. SSE (Server-Sent Events) provides real-time updates when Redis is available; a polling fallback keeps the UI functional when it is not.


Starting the UI

taskq ui serve

This starts a Uvicorn server bound to TASKQ_ADMIN_HOST:TASKQ_ADMIN_PORT (defaults: 0.0.0.0:8080) and serves the admin router at /admin.

The server reads configuration from the standard TASKQ_ environment variables (or .env files). See cli.md for the full command reference.

Environment variables

Variable Default Description
TASKQ_ADMIN_HOST 0.0.0.0 Bind address for the admin server.
TASKQ_ADMIN_PORT 8080 Bind port for the admin server.
TASKQ_ADMIN_URL http://localhost:8080 Public base URL as seen from a browser. Used by the example trigger app to build redirect URLs after enqueueing. Override when admin and trigger app are on different hosts or ports.
TASKQ_PG_DSN postgresql://taskq:taskq@localhost:5432/taskq Postgres connection string.
TASKQ_REDIS_URL (none) Optional. When set, enables real-time mode (SSE push) and live Redis state on the rate-limits page.
TASKQ_ADMIN_UI_POLLING_INTERVAL_SECONDS 2.0 Page refresh interval in polling and polling-degraded modes.
TASKQ_ADMIN_UI_ALLOW_RATE_LIMIT_RESET false When True, enables the reset button on the /rate-limits page.
TASKQ_SCHEMA_NAME taskq Postgres schema containing TaskQ tables.
TASKQ_ENVIRONMENT (none) Set to dev or development to bypass the fail-closed auth check (local development only).
TASKQ_ADMIN_UI_REQUIRE_AUTH true When true (the default), create_router() raises RuntimeError in non-dev environments if no auth_dependency is configured. Set to false to suppress the error and allow an unauthenticated admin UI behind a reverse proxy (not recommended unless you have an external auth layer).
TASKQ_ADMIN_ACTIONS_ENABLED false When true, enables destructive admin actions: job cancel, job retry, and schedule run-now. When false (the default), these endpoints return 403. Separate from auth_dependency, which controls read access to all admin routes.
TASKQ_ADMIN_MAX_SSE_CONNECTIONS 50 Per-topic cap on concurrent SSE connections.
TASKQ_HEALTH_TOKEN (none) Bearer token for machine-to-machine access to /jobs/health/* endpoints. When set, health and metrics routes require a matching Authorization: Bearer <token> header. Leave empty for unauthenticated cluster-internal access.
TASKQ_HEALTH_REQUIRE_TOKEN true When true (the default), taskq ui serve raises RuntimeError if TASKQ_HEALTH_TOKEN is empty in a non-dev environment, failing closed. Set to false to allow unauthenticated health/metrics in non-dev (e.g. when relying on network policy).

Docker Compose

services:
  taskq-admin:
    image: your-app
    command: taskq ui serve
    environment:
      TASKQ_PG_DSN: postgresql://taskq:taskq@postgres:5432/taskq
      TASKQ_REDIS_URL: redis://redis:6379/0
      TASKQ_ADMIN_HOST: "0.0.0.0"
      TASKQ_ADMIN_PORT: "8080"
    ports:
      - "8080:8080"

Security

Built-in SSO support

TaskQ ships optional OIDC and SAML single sign-on backends behind a shared abstraction. See SSO / SAML for Entra ID walkthroughs, configuration reference, and container requirements. TASKQ_SSO_BACKEND=none (the default) preserves the BYO-auth / reverse-proxy behavior described below.

Fail-closed by default

The admin UI fails closed in non-dev environments when no authentication is configured. admin_ui_require_auth defaults to True, so create_router() raises RuntimeError if auth_dependency=None and TASKQ_ENVIRONMENT is not dev or development. This prevents accidentally deploying an unauthenticated admin UI in production.

# The default — fails closed in non-dev:
TASKQ_ENVIRONMENT=production taskq ui serve
# RuntimeError: admin UI requires auth_dependency in non-dev environments

To opt out (e.g. when relying on a reverse proxy for authentication), set TASKQ_ADMIN_UI_REQUIRE_AUTH=false:

export TASKQ_ENVIRONMENT=production
export TASKQ_ADMIN_UI_REQUIRE_AUTH=false
taskq ui serve
# WARNING log: admin-ui-no-auth — but server starts

Dev environments (TASKQ_ENVIRONMENT=dev or development) bypass the check entirely — no RuntimeError, no warning. Use this only in local development:

TASKQ_ENVIRONMENT=development taskq ui serve

Protecting the router with FastAPI authentication

When embedding the admin router in your own FastAPI app, pass an auth_dependency callable. This is applied as a FastAPI Depends() to every route in the router:

from fastapi import Depends, HTTPException, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

bearer = HTTPBearer()

async def require_token(
    credentials: HTTPAuthorizationCredentials = Security(bearer),
) -> str:
    if credentials.credentials != "your-secret-admin-token":
        raise HTTPException(status_code=401, detail="invalid admin token")
    return credentials.credentials

# Pass to create_router():
bundle = create_router(
    pg_pool,
    schema="taskq",
    redis_client=None,
    auth_dependency=require_token,   # protects all routes
    base_path="/admin",
)

Any FastAPI dependency callable works — HTTPBearer, HTTPBasic, OAuth2, custom session middleware, etc.

Protecting taskq ui serve with a reverse proxy

When running taskq ui serve as a standalone process (no custom FastAPI app), place a reverse proxy in front that enforces authentication:

nginx example:

server {
    listen 443 ssl;
    server_name admin.example.com;

    # ... TLS config ...

    location /admin/ {
        auth_basic "TaskQ Admin";
        auth_basic_user_file /etc/nginx/.htpasswd;
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Caddy example (automatic HTTPS + basic auth):

admin.example.com {
    basicauth {
        admin $2a$14$...hashed-password...
    }
    reverse_proxy 127.0.0.1:8080
}

Rate-limit reset endpoint

The POST /rate-limits/{bucket_name}/reset endpoint is a write operation. It is disabled by default (TASKQ_ADMIN_UI_ALLOW_RATE_LIMIT_RESET=false). When enabled, it clears a rate-limit bucket to full capacity via a CSRF-protected form POST. Resets are logged at WARNING level.

Keep this disabled in production unless operators need fast incident-response capability. If enabled, ensure the authentication layer (middleware or reverse proxy) covers this endpoint — it is protected by the same auth_dependency as all other routes.

Job cancel and retry endpoints

The POST /jobs/{job_id}/cancel and POST /jobs/{job_id}/retry endpoints are write operations gated by TASKQ_ADMIN_ACTIONS_ENABLED (default false). When admin_actions_enabled is false, both endpoints return 403. Set TASKQ_ADMIN_ACTIONS_ENABLED=true to enable them. Both are CSRF-protected. Cancel writes a cancel request to Postgres; retry resets a terminal job to pending via backend.retry_job. Ensure the authentication layer covers these endpoints in production — they can modify job state.


Routes

All GET routes are read-only HTML pages. POST routes (cancel, retry, schedule management, rate-limit reset) are CSRF-protected write operations.

GET /admin/

Redirects (302) to /admin/queues.

GET /admin/queues

Queue overview. Lists all queues that have jobs in pending, scheduled, or running state. For each queue shows the count of jobs in each of those three statuses.

GET /admin/queues/{queue}

Queue detail page. Lists jobs in the named queue filtered by status (query parameter; allowed values: pending, scheduled, running; default: pending). Results are paginated at 100 rows using keyset pagination on (scheduled_at, id).

Query parameters:

Parameter Required Description
status No (default pending) Filter by job status.
cursor_at No ISO 8601 timestamp cursor for the next page. Must be provided together with cursor_id.
cursor_id No UUID cursor for the next page. Must be provided together with cursor_at.

Returns 400 if status is not an allowed value or if only one of cursor_at / cursor_id is provided.

GET /admin/history

Historical job list. Shows completed (terminal) jobs from both the live jobs table (not yet pruned) and the jobs_archive table (already pruned). Rows are ordered most-recent-first by finished_at. Results are paginated at 50 rows using keyset pagination on (finished_at DESC, id DESC).

A metrics bar at the top of the page shows the total result count (capped at "1000+" for large result sets), per-status counts, and the overall success rate for the filtered result set.

Query parameters:

Parameter Required Description
status No (default: all terminal) Filter by one or more terminal statuses. Repeatable: ?status=succeeded&status=failed. Allowed values: succeeded, failed, cancelled, crashed, abandoned.
actor No Exact match on actor name.
queue No Exact match on queue name.
cursor_at No ISO 8601 timestamp cursor for the next page. Must be provided together with cursor_id.
cursor_id No UUID cursor for the next page. Must be provided together with cursor_at.

Returns 400 if a status value is not a terminal status, or if only one of cursor_at / cursor_id is provided.

Archived rows (from jobs_archive) are shown with an "archived" badge in the Source column. Still-live terminal rows (not yet pruned from jobs) are shown as "live".

GET /admin/jobs

Job listing page with "Live Jobs" and "Archived" tabs. Supports filtering by status (multi-select), actor (substring match), queue, time range, identity key, fairness key, free-text search (matches job ID or actor), and tags. Results are paginated at 100 rows using keyset pagination and can be sorted by created_at, actor, queue, status, or attempt. HTMX partial refreshes update the table without a full page reload. A live SSE stream (/admin/jobs/sse/live) pushes state-change events for real-time badge updates.

Query parameters (selected):

Parameter Default Description
tab live live (jobs table) or archived (jobs_archive table).
status all (live) or terminal (archived) Repeatable status filter: ?status=pending&status=running.
actor Substring match on actor name (ILIKE).
queue Exact match on queue name.
sort / order created_at / desc Sort column and direction.
cursor_at / cursor_id Keyset pagination cursor (both required together).

GET /admin/jobs/{job_id}

Job detail. Shows the full job record, attempt history from job_attempts, and the event log from job_events. Tracebacks are truncated to 2 000 characters with a (N more characters) suffix. Returns 404 if the job does not exist.

If the job has already been pruned to jobs_archive, the page loads from the archive table instead; attempt history comes from job_attempts_archive and the event log is empty (events are not archived). An "archived" banner is shown at the top of the page.

The job detail page includes a Cancel button (for non-terminal jobs) and a Retry button (for terminal jobs in failed, crashed, or cancelled state). Both are CSRF-protected POST forms. When admin_actions_enabled is false (the default), both buttons return 403 on submit; set TASKQ_ADMIN_ACTIONS_ENABLED=true to enable them.

POST /admin/jobs/{job_id}/cancel

Cancels a non-terminal job by writing a cancel request via backend.write_cancel_request. The heartbeat loop will observe the cancel flag and drive the three-phase cancellation protocol. Returns 403 if admin_actions_enabled is false, 404 if the job does not exist, 409 if the job is already in a terminal state. Redirects to the job detail page on success.

POST /admin/jobs/{job_id}/retry

Resets a terminal job (failed, crashed, or cancelled) back to pending via backend.retry_job, allowing it to be re-dispatched by a worker. Returns 403 if admin_actions_enabled is false, 404 if the job does not exist, 409 if the job is not in a retryable state. Redirects to the job detail page on success. The retry resets attempt to 0, clears error fields, and sets status='pending'.

GET /admin/jobs/count

Returns {"count": <int>} for the given tab (live or archived) and the same filter query params as GET /admin/jobs (status, actor, queue, time_range/time_from/time_to). Used by the jobs list page to render the result count without re-fetching the full page.

GET /admin/api/history/stats

Per-actor metrics as JSON. Returns aggregate execution statistics for all completed jobs in jobs_archive, grouped by (actor, queue). Does not paginate; returns at most 200 rows ordered by total job count descending.

Response shape:

{
  "actors": [
    {
      "actor": "send_email",
      "queue": "email",
      "total": 18420,
      "succeeded": 18100,
      "failed": 210,
      "cancelled": 80,
      "crashed": 20,
      "abandoned": 10,
      "avg_duration_ms": 340,
      "p50_duration_ms": 280,
      "p95_duration_ms": 950
    }
  ]
}

Duration percentiles are derived from job_attempts_archive.duration_ms via percentile_cont. Actors with no recorded attempt rows will have null for duration fields.

GET /admin/workers

Workers overview. Lists all rows from the workers table ordered by last_seen_at DESC, with an is_leader flag computed by a LEFT JOIN on maintenance_leader.

GET /admin/leader

Maintenance leader detail. Shows the current leader worker (hostname, pid, last seen). If no leader is elected, renders the template with leader=None. A watchdog-health indicator marks the leader as healthy when last_seen_at is within 30 seconds of the current wall-clock time.

GET /admin/schedules

Cron schedule list. Reads from cron_schedules ordered by next_fire_at. If the table does not exist (cron migration not yet applied), renders with a notice: "cron scheduling not installed — run taskq migrate up to enable".

Each schedule row includes buttons for the following CSRF-protected POST operations:

POST /admin/schedules/{schedule_id}/enable

Sets enabled=true, resets consecutive_failures to 0, and clears last_fire_error. Returns 404 if the schedule does not exist.

POST /admin/schedules/{schedule_id}/disable

Sets enabled=false. Returns 404 if the schedule does not exist.

POST /admin/schedules/{schedule_id}/skip

Advances next_fire_at to the next computed fire time after the current one. Repeatedly advances until next_fire_at is in the future (up to 1000 iterations; returns 400 if the cron expression produces no future fire time). Returns 404 if the schedule does not exist.

POST /admin/schedules/{schedule_id}/run

Enqueues a job for the schedule's actor immediately, using the schedule's payload_factory and the actor's stored actor_config row for queue, max_attempts, and retry_kind. Returns 403 if admin_actions_enabled is false, 404 if the schedule does not exist, 303 redirect with an error query parameter if the payload factory fails or the actor is not configured. A per-process 10-second cooldown prevents rapid re-triggering of the same schedule.

Per-process cooldown — not distributed

The run-now cooldown is tracked in-process (asyncio loop time), not in Postgres or Redis. In multi-replica deployments, each process has its own cooldown timer, so N replicas get N× the trigger rate. If you need a distributed cooldown, enforce it at the application layer or via an external rate limiter.

GET /admin/rate-limits

Rate-limit state page. Reads all rows from rate_limit_buckets (bucket name, kind, state JSON, updated timestamp). When TASKQ_REDIS_URL is configured, also fetches live Redis hash state for each bucket and displays it alongside the Postgres state. If Redis is unavailable at render time, falls back to Postgres-only state without raising an error.

Reset button. When TASKQ_ADMIN_UI_ALLOW_RATE_LIMIT_RESET=true (default false), each bucket row shows a reset button that clears the bucket to full capacity via a CSRF-protected POST to /rate-limits/{bucket_name}/reset. Resets are logged at WARNING level. Keep disabled in production unless operators need fast incident-response capability.

GET /admin/reservations

Reservation slot summary. For each bucket_name in reservation_slots, shows the count of held slots (where job_id IS NOT NULL), free slots, and total slots.

GET /admin/sse/{topic}

SSE (Server-Sent Events) endpoint. Accepts any topic string. On connect it emits an initial event: status frame with {"status": "awaiting_progress_backend"}, then sends : keepalive comments every 30 seconds to prevent connection timeout. See Real-time vs polling mode below.

GET /admin/static/{path}

Serves static assets (CSS, JS, images) from the bundled static directory. Path traversal is prevented: requests whose resolved path falls outside the static directory return 404.

Health routes

taskq ui serve mounts lightweight health endpoints at /jobs/health/:

Route Response Description
GET /jobs/health/live JSON {"status": "ok"} (200) or {"status": "unresponsive"} (503) Liveness probe (event-loop responsiveness check).
GET /jobs/health/ready JSON readiness report (200 or 503) Readiness probe including Postgres ping.
GET /jobs/health/metrics Prometheus text format (200) Prometheus metrics (requires taskq[prometheus]).

These endpoints use a lightweight PG pool ping for readiness (not the full WorkerDeps health report that the worker process serves on its Unix socket). The Prometheus metrics endpoint is mounted automatically when taskq[prometheus] is installed.

Protecting health endpoints with a bearer token

Set TASKQ_HEALTH_TOKEN to require a matching Authorization: Bearer <token> header on all health and metrics routes. This is intended for machine-to-machine access (Prometheus scrapers, kubelet probes, CI scripts) where an interactive OIDC/SAML login flow isn't practical:

export TASKQ_HEALTH_TOKEN='$(python -c "import secrets; print(secrets.token_urlsafe(32))")'
taskq ui serve

When TASKQ_HEALTH_TOKEN is empty (the default), health and metrics endpoints are unauthenticated — standard for cluster-internal endpoints behind a network policy. However, in non-dev environments (TASKQ_ENVIRONMENT not set to dev or development), taskq ui serve fails closed: it raises RuntimeError if TASKQ_HEALTH_TOKEN is empty and TASKQ_HEALTH_REQUIRE_TOKEN is true (the default). This prevents accidentally deploying health/metrics endpoints wide open.

To explicitly allow unauthenticated health/metrics in non-dev (e.g. when relying on network policy or cluster-internal-only access):

export TASKQ_ENVIRONMENT=production
export TASKQ_HEALTH_REQUIRE_TOKEN=false
taskq ui serve

k8s liveness/readiness probes

When TASKQ_HEALTH_TOKEN is set, k8s liveness/readiness probes must be configured to send the bearer token, or set TASKQ_HEALTH_REQUIRE_TOKEN=false to explicitly disable the requirement. Many k8s probe configurations don't send auth headers by default.


Real-time vs polling mode

The admin UI automatically selects its update strategy based on Redis availability, and shows a badge in the top-right corner of every page indicating the current mode.

Three-state badge

Badge label data-mode value Meaning
real-time mode realtime Redis is configured and reachable. Pages update via SSE (EventSource).
polling mode polling No TASKQ_REDIS_URL configured. Pages refresh by polling Postgres on an interval.
polling mode (Redis unavailable) polling-degraded TASKQ_REDIS_URL is set but Redis is currently unreachable. Automatic fallback to Postgres polling.

The server re-checks Redis health every 5 seconds (cached per process). The badge reflects the result of the most recent check.

Real-time mode (Redis configured)

When Redis is available, the page JS opens an EventSource connection to GET /admin/sse/{topic}. Updates are pushed over that connection, which triggers HTMX partial-page refreshes without a full reload.

If the EventSource connection emits an error, the JS closes it and automatically falls back to Postgres polling at TASKQ_ADMIN_UI_POLLING_INTERVAL_SECONDS intervals. The badge transitions to polling-degraded. A 30-second health heartbeat is sent on the SSE connection to keep it alive through proxies that would otherwise time out idle connections.

Polling mode (no Redis)

When Redis is not configured, the page JS polls Postgres directly using HTMX hx-trigger="every Ns". The poll interval is controlled by TASKQ_ADMIN_UI_POLLING_INTERVAL_SECONDS (default 2.0 s). All pages remain fully functional; data is just slightly less fresh than in real-time mode.

Configuration

Variable Default Description
TASKQ_REDIS_URL (none) When set, enables real-time mode. Must be a valid Redis URL (e.g. redis://localhost:6379/0).
TASKQ_ADMIN_UI_POLLING_INTERVAL_SECONDS 2.0 Page refresh interval (seconds) in polling and polling-degraded modes. Minimum: 0.1 s.
TASKQ_ADMIN_MAX_SSE_CONNECTIONS 50 Per-topic cap on concurrent SSE connections in real-time mode.

SSE connection limit

The GET /admin/sse/{topic} endpoint bounds concurrent connections per topic with an independent asyncio.Semaphore sized to TASKQ_ADMIN_MAX_SSE_CONNECTIONS. Valid topics are jobs, workers, queues, and history; each gets its own semaphore.

  • Default: 50
  • Minimum: 1 (validated at settings load time)
  • When the semaphore is full, new connections receive 429 Too Many Requests immediately.

/admin/jobs/sse/live has no connection cap

The jobs-list live-refresh endpoint GET /admin/jobs/sse/live is a separate route from GET /admin/sse/{topic}. It backs the LISTEN/NOTIFY stream that drives real-time job-table badge updates and has no semaphore and no connection limit. Only the generic /admin/sse/{topic} routes described above are capped by TASKQ_ADMIN_MAX_SSE_CONNECTIONS. If you need to bound live-refresh connections, enforce the limit at the reverse proxy or load balancer layer.


Frontend architecture

Technology stack

Layer Technology Role
Templating Jinja2 (partials via {% include %}) Server-rendered HTML fragments
Reactivity Alpine.js 3.x jobsPage, statusCombobox, dark mode toggle
Partial updates HTMX 2.x hx-get, hx-target, hx-trigger for AJAX table refreshes
Real-time push SSE (EventSource) State-change events pushed from server
Icons Lucide Feather-compatible SVG icons
Styling Tailwind CSS (utility classes inlined) Dark-mode-aware responsive layout

Alpine.js components

jobsPage (admin.js:47-185) — the main job listing page component. Registered via Alpine.data("jobsPage", ...) and wired in jobs.html with x-data="jobsPage". Configuration is passed from the Jinja2 template via window.__taskqJobConfig, set in a <script> block in the page's {% block head %}.

Key features: - Tab switching (switchTab) — switches between "Live Jobs" and "Archived" views by submitting the filter form with tab parameter. - Live refresh toggle — when enabled, connects to SSE for real-time state-change events; when paused, polls on poll_interval_ms via setInterval. Pending-count badge shows accumulated events while paused. - SSE integrationconnectSSE() opens an EventSource to {base_path}/sse/jobs. On error, falls back to polling. On state_change events, updates the matching table row's status badge or increments the pending counter. - Table refreshrefreshTable() fetches {base_path}/jobs with current filter parameters via HTMX (HX-Request: true header), swaps the #job-table-container element.

statusCombobox (admin.js:187-217) — a reactive multi-select dropdown for job status filtering. Supports Select All, Active, Terminal, and Clear presets. Statuses are rendered with color-coded classes from STATUS_COLORS and CHIP_COLORS lookup maps.

Partial templates

Reusable Jinja2 partials live in src/taskq/web/templates/_partials/:

File Purpose
job_table.html Full job listing table with sortable headers, pagination, status badges, progress bars, and tag chips. Included via {% include %} from jobs.html.
job_card.html status_badge, duration_fmt, and timestamp_cell macros shared by job_table.html and job_detail.html.
table.html Generic styled_table macro with table_header, table_body, table_row, and table_cell call blocks. Used by workers.html, queues.html, and other list pages for consistent styling.
sse_console.html SSE console panel (used for debugging real-time connections).

Real-time mode badge

Every page displays a mode badge in the top-right corner of the header (set in _base.html:35). The badge's data-mode attribute and label are driven by realtime_mode and mode_label template variables injected by route handlers.

Badge label data-mode Meaning
real-time mode realtime Redis configured and reachable; SSE push active
polling mode polling No TASKQ_REDIS_URL configured; HTMX polling
polling mode (Redis unavailable) polling-degraded Redis configured but unreachable; automatic fallback

The server re-checks Redis health every 5 seconds (cached per-process in _factory.py:_RedisHealthCache). The badge reflects the most recent check.

The dark mode toggle (in _base.html:37) persists preference to localStorage and uses Alpine's x-data and x-init for immediate class application (no flash of unstyled content on page load).


create_router() — embedding in your own FastAPI app

If you have an existing FastAPI application, you can mount the admin router directly instead of running taskq ui serve.

from taskq.web.admin import create_router

router = create_router(
    pg_pool,
    schema="taskq",
    redis_client=None,        # pass a redis.asyncio.Redis instance to enable live Redis state
    auth_dependency=None,     # pass a FastAPI dependency callable to protect all routes
    base_path="/admin",
    backend=None,             # pass an existing Backend to reuse it instead of constructing one
)
Parameter Type Default Description
pg_pool asyncpg.Pool required Asyncpg connection pool. Must be open for the lifetime of the router.
schema str "taskq" Postgres schema. Must match [A-Za-z_][A-Za-z0-9_]*; raises ValueError otherwise.
redis_client redis.asyncio.Redis \| None None Optional Redis client. Enables live Redis state on the rate-limits page.
auth_dependency Callable \| None None FastAPI dependency applied to all routes. If None and TASKQ_ENVIRONMENT is not dev/development, create_router() raises RuntimeError (default fail-closed). Set TASKQ_ADMIN_UI_REQUIRE_AUTH=false to suppress the error and allow unauthenticated access behind a reverse proxy.
base_path str "" Must match the prefix passed to include_router. Injected as a Jinja2 global so templates build correct URLs.
backend Backend \| None None Optional pre-built Backend to reuse (e.g. one already created by your JobsClient). When None, the router builds its own PostgresBackend from pg_pool/schema.

create_router() returns an AdminBundle containing the router and all values needed for app.state. Call it inside your lifespan so the pool is already open, then populate app.state via setup_admin_state() and mount the router:

from contextlib import asynccontextmanager
import asyncpg
from fastapi import FastAPI
from taskq.settings import TaskQSettings
from taskq.web.admin import create_router, setup_admin_state

@asynccontextmanager
async def lifespan(app: FastAPI):
    settings = TaskQSettings.load()
    pool = await asyncpg.create_pool(str(settings.pg_dsn))
    try:
        bundle = create_router(
            pool,
            schema=settings.schema_name,
            redis_client=None,        # pass a redis.asyncio.Redis instance for live state
            auth_dependency=None,     # pass a FastAPI dependency to protect all routes
            base_path="/admin",
        )
        setup_admin_state(app, bundle)          # populates app.state.pg_pool, .schema, etc.
        app.include_router(bundle.router, prefix="/admin")
        yield
    finally:
        await pool.close()

app = FastAPI(lifespan=lifespan)

setup_admin_state() writes pg_pool, schema, redis_client, templates, settings, base_path, and backend onto app.state. Route handlers resolve these via Depends(get_pg_pool), Depends(get_templates), etc. You do not need to set app.state manually — setup_admin_state() handles it.