{"id":47700631,"url":"https://github.com/yakser/asynqpg","last_synced_at":"2026-05-09T14:17:46.342Z","repository":{"id":345610925,"uuid":"1182351369","full_name":"yakser/asynqpg","owner":"yakser","description":"Distributed task queue for Go backed by PostgreSQL – transactional enqueue, retry, DLQ, web dashboard, and OpenTelemetry.","archived":false,"fork":false,"pushed_at":"2026-03-27T22:24:28.000Z","size":1534,"stargazers_count":1,"open_issues_count":9,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-03-28T04:59:17.740Z","etag":null,"topics":["background-jobs","go","golang","postgres","postgresql","queue","task-queues"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yakser.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-15T12:01:23.000Z","updated_at":"2026-03-25T22:37:23.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/yakser/asynqpg","commit_stats":null,"previous_names":["yakser/asynqpg"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/yakser/asynqpg","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yakser%2Fasynqpg","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yakser%2Fasynqpg/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yakser%2Fasynqpg/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yakser%2Fasynqpg/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yakser","download_url":"https://codeload.github.com/yakser/asynqpg/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yakser%2Fasynqpg/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31311217,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T12:59:32.332Z","status":"ssl_error","status_checked_at":"2026-04-02T12:54:48.875Z","response_time":89,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["background-jobs","go","golang","postgres","postgresql","queue","task-queues"],"created_at":"2026-04-02T17:08:39.162Z","updated_at":"2026-05-09T14:17:46.322Z","avatar_url":"https://github.com/yakser.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# asynqpg\n\nDistributed task queue for Go, backed by PostgreSQL.\n\n[![Tests](https://github.com/yakser/asynqpg/actions/workflows/tests.yml/badge.svg)](https://github.com/yakser/asynqpg/actions/workflows/tests.yml)\n![Coverage](https://img.shields.io/badge/coverage-83.5%25-green)\n[![Go Reference](https://pkg.go.dev/badge/github.com/yakser/asynqpg.svg)](https://pkg.go.dev/github.com/yakser/asynqpg)\n[![Website](https://img.shields.io/badge/website-asynqpg--landing-blue)](https://yakser.github.io/asynqpg-landing)\n\nConsumers pull tasks concurrently without contention – Postgres hands each worker its own batch and skips rows already claimed by others, so throughput scales with workers. Producers can enqueue inside the same transaction as your business logic, so a job appears if and only if your write commits.\n\n## Contents\n\n- [Features](#features)\n- [Installation](#installation)\n- [Database Setup](#database-setup)\n- [Quick Start](#quick-start)\n- [Architecture](#architecture)\n- [Producer](#producer)\n- [Consumer](#consumer)\n- [Client](#client)\n- [Web UI](#web-ui)\n- [Observability](#observability)\n- [Demo](#demo)\n- [Performance](#performance)\n- [Testing](#testing)\n- [Contributing](#contributing)\n- [Support](#support)\n- [Project Status](#project-status)\n- [License](#license)\n\n## Features\n\n- **Postgres-native** – tasks stored in a single table; no Redis or external broker required\n- **Safe concurrent processing** – Postgres hands each worker its own batch and skips rows already claimed by others, so multiple consumers never touch the same task\n- **Transactional enqueue** – pass `*sqlx.Tx` to `EnqueueTx` so jobs commit atomically with your business logic\n- **Flexible retries** – exponential or constant backoff, snooze without consuming attempts, skip-retry for permanent errors\n- **Delayed \u0026 scheduled tasks** – `WithDelay` or direct `ProcessAt` for future processing\n- **Idempotency tokens** – `EnqueueMany` deduplicates by `(type, idempotency_token)` at the DB layer\n- **Per-type worker pools** – independent concurrency, timeout, and middleware per task type\n- **Leader-elected maintenance** – a single node holds the lease for stuck-task rescue and cleanup\n- **Web dashboard** – embedded React SPA with Overview, Tasks, Workers, and Maintenance pages; ⌘K palette and `j`/`k` navigation\n- **Pluggable auth** – Basic Auth out of the box; OAuth providers (GitHub included)\n- **OpenTelemetry** – built-in counters, histograms, and distributed tracing for all operations\n\n## Installation\n```bash\ngo get github.com/yakser/asynqpg\n```\n\nRequires Go 1.25+ and PostgreSQL 14+.\n\n## Database Setup\n\nApply migrations to create the `asynqpg_tasks` table:\n\n```bash\nmake up         # start PostgreSQL in Docker\nmake migrate    # apply migrations\n```\n\nOr use `testcontainers-go` in tests – no manual setup needed.\n\n## Quick Start\n\n### Producer\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"encoding/json\"\n    \"log\"\n    \"time\"\n\n    \"github.com/jmoiron/sqlx\"\n    _ \"github.com/lib/pq\"\n    \"github.com/yakser/asynqpg\"\n    \"github.com/yakser/asynqpg/producer\"\n)\n\nfunc main() {\n    db, err := sqlx.Connect(\"postgres\", \"postgres://postgres:password@localhost:5432/asynqpg?sslmode=disable\")\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    p, err := producer.New(producer.Config{Pool: db})\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    payload, _ := json.Marshal(map[string]string{\"to\": \"user@example.com\", \"subject\": \"Hello\"})\n\n    _, err = p.Enqueue(context.Background(), asynqpg.NewTask(\"email:send\", payload,\n        asynqpg.WithMaxRetry(5),\n        asynqpg.WithDelay(10*time.Second),\n    ))\n    if err != nil {\n        log.Fatal(err)\n    }\n}\n```\n\n### Consumer\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"log\"\n    \"os\"\n    \"os/signal\"\n    \"syscall\"\n    \"time\"\n\n    \"github.com/jmoiron/sqlx\"\n    _ \"github.com/lib/pq\"\n    \"github.com/yakser/asynqpg\"\n    \"github.com/yakser/asynqpg/consumer\"\n)\n\nfunc main() {\n    db, err := sqlx.Connect(\"postgres\", \"postgres://postgres:password@localhost:5432/asynqpg?sslmode=disable\")\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    c, err := consumer.New(consumer.Config{Pool: db})\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    if err := c.RegisterTaskHandler(\"email:send\",\n        consumer.TaskHandlerFunc(func(ctx context.Context, task *asynqpg.TaskInfo) error {\n            fmt.Printf(\"Processing task %d: %s\\n\", task.ID, task.Type)\n            // process task...\n            return nil\n        }),\n        consumer.WithWorkersCount(5),\n        consumer.WithTimeout(30*time.Second),\n    ); err != nil {\n        log.Fatal(err)\n    }\n\n    if err := c.Start(); err != nil {\n        log.Fatal(err)\n    }\n\n    sigCh := make(chan os.Signal, 1)\n    signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)\n    \u003c-sigCh\n\n    if err := c.Stop(); err != nil {\n        log.Printf(\"shutdown error: %v\", err)\n    }\n}\n```\n\n## Architecture\n\n### Task Lifecycle\n\n```mermaid\nflowchart LR\n    %% Styles\n    classDef pending   fill:#f59e0b,stroke:#92400e,color:#000\n    classDef running   fill:#3b82f6,stroke:#1e40af,color:#fff\n    classDef completed fill:#22c55e,stroke:#166534,color:#000\n    classDef failed    fill:#ef4444,stroke:#991b1b,color:#fff\n    classDef cancelled fill:#6b7280,stroke:#374151,color:#fff\n    classDef edge      fill:#f3f4f6,stroke:#d1d5db,color:#374151\n\n    %% States\n    P[\"Pending\"]:::pending\n    T[\"Running\"]:::running\n\n    subgraph Final\n        C[\"Completed\"]:::completed\n        F[\"Failed\"]:::failed\n        Ca[\"Cancelled\"]:::cancelled\n    end\n\n    %% Entry points\n    Enqueue([\"Enqueue\"]):::edge --\u003e P\n    EnqueueDelayed([\"Enqueue (with delay)\"]):::edge --\u003e P\n\n    %% Normal processing\n    P -- \"fetched by consumer\" --\u003e T\n    T -- \"handler success\" --\u003e C\n    T -- \"handler error, attempts_left \u003e 0\" --\u003e P\n    T -- \"handler error, attempts_left = 0\" --\u003e F\n\n    %% Rescuer (stuck tasks)\n    T -- \"stuck, attempts_left \u003e 0 (Rescuer)\" --\u003e P\n    T -- \"stuck, attempts_left = 0 (Rescuer)\" --\u003e F\n\n    %% Manual actions via client API\n    F  -- \"manual retry\" --\u003e P\n    Ca -- \"manual retry\" --\u003e P\n    P  -- \"manual cancel\" --\u003e Ca\n    F  -- \"manual cancel\" --\u003e Ca\n\n    %% Cleaner (maintenance, leader-only)\n    C  -. \"auto-delete (Cleaner)\" .-\u003e Deleted([\"Deleted\"]):::edge\n    F  -. \"auto-delete (Cleaner)\" .-\u003e Deleted\n    Ca -. \"auto-delete (Cleaner)\" .-\u003e Deleted\n\n    %% Manual delete via client API\n    P  -- \"manual delete\" --\u003e Deleted\n    C  -- \"manual delete\" --\u003e Deleted\n    F  -- \"manual delete\" --\u003e Deleted\n    Ca -- \"manual delete\" --\u003e Deleted\n```\n\nTasks are fetched using `SELECT ... FOR NO KEY UPDATE SKIP LOCKED`, enabling safe concurrent processing across multiple consumers. The `blocked_till` column serves as both a delay mechanism (delayed enqueue, retry backoff) and a distributed lock expiry – a task re-appears for fetching only after `blocked_till` passes.\n\n### Packages\n\n| Package | Purpose |\n|---|---|\n| `producer/` | Enqueue tasks: `Enqueue`, `EnqueueTx`, `EnqueueMany`, `EnqueueManyTx` |\n| `consumer/` | Fetch and process tasks with configurable worker pools |\n| `client/` | Task inspection and management (get, list, cancel, retry, delete) |\n| `ui/` | HTTP handler serving REST API + embedded React SPA |\n\n## Producer\n\nCreate a producer with `producer.New`:\n\n```go\np, err := producer.New(producer.Config{\n    Pool:            db,              // required: *sqlx.DB\n    Logger:          slog.Default(),  // optional\n    DefaultMaxRetry: 3,               // optional, default: 3\n    MeterProvider:   mp,              // optional: OTel metrics\n    TracerProvider:  tp,              // optional: OTel traces\n})\n```\n\n### Enqueue Methods\n\n```go\n// Single task\nid, err := p.Enqueue(ctx, task)\n\n// Within an existing transaction (atomic with your business logic)\nid, err = p.EnqueueTx(ctx, tx, task)\n\n// Batch enqueue (skips duplicates by idempotency token)\nids, err := p.EnqueueMany(ctx, tasks)\n\n// Batch within a transaction\nids, err := p.EnqueueManyTx(ctx, tx, tasks)\n```\n\n### Task Options\n\n```go\nasynqpg.NewTask(\"type\", payload,\n    asynqpg.WithMaxRetry(5),                       // max retry attempts\n    asynqpg.WithDelay(10*time.Second),              // delay before first processing\n    asynqpg.WithIdempotencyToken(\"unique-token\"),   // deduplicate enqueues\n)\n\n// Schedule a task for a specific time\ntask := asynqpg.NewTask(\"report:generate\", payload)\ntask.ProcessAt = time.Date(2026, 1, 1, 9, 0, 0, 0, time.UTC)\n```\n\n## Consumer\n\nCreate a consumer with `consumer.New`:\n\n```go\nc, err := consumer.New(consumer.Config{\n    Pool:              db,               // required\n    ClientID:          \"worker-1\",       // optional, for leader election\n    RetryPolicy:       retryPolicy,      // optional, default: exponential backoff\n    FetchInterval:     100*time.Millisecond,\n    ShutdownTimeout:   30*time.Second,\n    // Retention for completed/failed/cancelled tasks\n    CompletedRetention: 24*time.Hour,\n    FailedRetention:    7*24*time.Hour,\n    CancelledRetention: 24*time.Hour,\n\n    // DisableMaintenance: true,    // set to disable rescuer + cleaner\n    // DisableBatchCompleter: true, // set to disable batch completions\n})\n```\n\n### Handler Registration\n\n```go\nerr := c.RegisterTaskHandler(\"email:send\", handler,\n    consumer.WithWorkersCount(10),          // goroutines for this task type\n    consumer.WithMaxAttempts(5),            // override default\n    consumer.WithTimeout(30*time.Second),   // per-task execution timeout\n)\n```\n\nImplement `consumer.TaskHandler` or use the `TaskHandlerFunc` adapter:\n\n```go\ntype TaskHandler interface {\n    Handle(ctx context.Context, task *asynqpg.TaskInfo) error\n}\n```\n\n### Retry Policies\n\n**Exponential backoff** (default) – `attempt^4` seconds with 10% jitter, capped at 24h:\n\n```go\n\u0026asynqpg.DefaultRetryPolicy{MaxRetryDelay: 24 * time.Hour}\n```\n\n**Constant delay:**\n\n```go\n\u0026asynqpg.ConstantRetryPolicy{Delay: 5 * time.Second}\n```\n\n### SkipRetry\n\nIf a handler encounters a permanent error that should not be retried (e.g., invalid payload, business logic rejection), it can return `asynqpg.ErrSkipRetry` to immediately fail the task, skipping all remaining retry attempts:\n\n```go\nfunc (h *EmailHandler) Handle(ctx context.Context, task *asynqpg.TaskInfo) error {\n    var payload EmailPayload\n    if err := json.Unmarshal(task.Payload, \u0026payload); err != nil {\n        // Invalid payload – retrying won't help\n        return fmt.Errorf(\"bad payload: %w\", asynqpg.ErrSkipRetry)\n    }\n    // process task...\n    return nil\n}\n```\n\n`SkipRetry` works with `errors.Is`, so it can be wrapped with additional context via `fmt.Errorf(\"...: %w\", asynqpg.ErrSkipRetry)`.\n\n### Snooze\n\nSometimes a handler needs to defer processing without counting it as a failure. `TaskSnooze` reschedules the task after a given duration **without** counting it as an attempt – `attempts_left` and `attempts_elapsed` remain unchanged:\n\n```go\nfunc (h *MyHandler) Handle(ctx context.Context, task *asynqpg.TaskInfo) error {\n    if !isExternalServiceReady() {\n        // Try again in 30 seconds, doesn't count as a failed attempt\n        return asynqpg.TaskSnooze(30 * time.Second)\n    }\n    // process task...\n    return nil\n}\n```\n\nIf you want to reschedule **and** count it as a failed attempt (with error message stored), use `TaskSnoozeWithError`:\n\n```go\nfunc (h *MyHandler) Handle(ctx context.Context, task *asynqpg.TaskInfo) error {\n    if err := callExternalAPI(); err != nil {\n        // Retry in 1 minute, counts as an attempt, error message is stored\n        return fmt.Errorf(\"api unavailable: %w\", asynqpg.TaskSnoozeWithError(1 * time.Minute))\n    }\n    // process task...\n    return nil\n}\n```\n\nKey differences:\n\n| | `TaskSnooze` | `TaskSnoozeWithError` |\n|---|---|---|\n| Counts as attempt | No | Yes |\n| Stores error message | No | Yes |\n| Respects max retries | No (unlimited snoozes) | Yes (fails when exhausted) |\n| Use case | External dependency not ready | Transient error with custom delay |\n\nBoth work with `errors.As` and can be wrapped with `fmt.Errorf`. Panics on negative duration; zero duration makes the task immediately available.\n\n### Task vs TaskInfo\n\nThe library uses two distinct structs to separate concerns:\n\n- **`Task`** (root package) -- the input struct for enqueueing. Contains only fields you set when creating a task: `Type`, `Payload`, `Delay`, `MaxRetry`, `IdempotencyToken`.\n- **`TaskInfo`** (root package) -- the runtime struct passed to handlers. Contains all database-assigned fields needed during processing:\n\n```go\nfunc (h *MyHandler) Handle(ctx context.Context, task *asynqpg.TaskInfo) error {\n    task.ID               // database ID\n    task.Type             // task type\n    task.Payload          // task payload\n    task.AttemptsLeft     // remaining retry attempts\n    task.AttemptsElapsed  // number of attempts already made\n    task.CreatedAt        // when the task was first enqueued\n    task.Messages         // error messages from previous failed attempts\n    task.AttemptedAt      // when the current processing attempt started\n    // ...\n}\n```\n\nThe `client` package has its own `client.TaskInfo` -- a full database read model returned by `client.GetTask` / `client.ListTasks` for inspection and management. It includes additional fields like `Status`, `BlockedTill`, `UpdatedAt`, and `FinalizedAt`.\n\n### Context Utilities\n\nTask metadata is also available via context helpers, useful in middleware and utilities:\n\n```go\nfunc (h *MyHandler) Handle(ctx context.Context, task *asynqpg.TaskInfo) error {\n    id, _        := asynqpg.GetTaskID(ctx)         // database ID\n    retry, _     := asynqpg.GetRetryCount(ctx)     // attempts already elapsed\n    max, _       := asynqpg.GetMaxRetry(ctx)       // total max retry count\n    createdAt, _ := asynqpg.GetCreatedAt(ctx)       // task creation time\n\n    // Or get all metadata at once:\n    meta, ok := asynqpg.GetTaskMetadata(ctx)\n    // meta.ID, meta.RetryCount, meta.MaxRetry, meta.CreatedAt\n    // ...\n}\n```\n\nFor testing handlers, use `asynqpg.WithTaskMetadata` to create a context with metadata:\n\n```go\nctx := asynqpg.WithTaskMetadata(context.Background(), asynqpg.TaskMetadata{\n    ID: 42, RetryCount: 0, MaxRetry: 3, CreatedAt: time.Now(),\n})\nerr := handler.Handle(ctx, task)\n```\n\n### Middleware\n\nThe consumer supports composable middleware for cross-cutting concerns. Middleware wraps task handlers using the `func(TaskHandler) TaskHandler` pattern, similar to `net/http` middleware.\n\n**Global middleware** applies to all task types:\n\n```go\nc, _ := consumer.New(config)\n\n_ = c.Use(func(next consumer.TaskHandler) consumer.TaskHandler {\n    return consumer.TaskHandlerFunc(func(ctx context.Context, task *asynqpg.TaskInfo) error {\n        slog.Info(\"processing task\", \"type\", task.Type, \"id\", task.ID)\n        err := next.Handle(ctx, task)\n        slog.Info(\"task done\", \"type\", task.Type, \"id\", task.ID, \"error\", err)\n        return err\n    })\n})\n```\n\n**Per-task-type middleware** applies only to a specific handler:\n\n```go\nc.RegisterTaskHandler(\"email:send\", emailHandler,\n    consumer.WithMiddleware(rateLimitMiddleware),\n    consumer.WithWorkersCount(5),\n)\n```\n\nExecution order: global middleware (outermost, first registered runs first) wraps per-task middleware, which wraps the handler. A middleware can short-circuit by returning an error without calling `next.Handle`.\n\n### Lifecycle\n\n```go\nc.Start()               // start processing\nc.Stop()                 // graceful shutdown (uses configured ShutdownTimeout)\nc.Shutdown(timeout)      // graceful shutdown with custom timeout\n```\n\n## Client\n\nInspect and manage tasks:\n\n```go\ncl, err := client.New(client.Config{Pool: db})\n\n// Get a single task\ninfo, err := cl.GetTask(ctx, taskID)\n\n// List tasks with filtering\nresult, err := cl.ListTasks(ctx, client.NewListParams().\n    States(asynqpg.TaskStatusFailed, asynqpg.TaskStatusPending).\n    Types(\"email:send\").\n    Limit(50).\n    OrderBy(client.OrderByCreatedAt, client.SortDesc),\n)\n// result.Tasks, result.Total\n\n// Manage tasks\n_, err = cl.CancelTask(ctx, id)   // pending/failed → cancelled\n_, err = cl.RetryTask(ctx, id)    // failed/cancelled → pending\n_, err = cl.DeleteTask(ctx, id)   // remove from database\n```\n\nAll methods have `*Tx` variants for transactional use.\n\n## Web UI\n\nMount the dashboard as an HTTP handler:\n\n```go\nhandler, err := ui.NewHandler(ui.HandlerOpts{\n    Pool:   db,\n    Prefix: \"/asynqpg\",\n})\nhttp.Handle(\"/asynqpg/\", handler)\n```\n\nThe dashboard is a single-page React app embedded in the Go binary via `//go:embed`. It ships as four operator pages plus a profile/appearance page, with a command palette (⌘K), keyboard navigation (`j`/`k` to move, `x` to select), and light/dark themes that follow the system preference.\n\n### Overview\n\nCluster snapshot at a glance: KPIs by status, per-task-type breakdown, current leader, and lease TTL. Auto-refreshes every 5 seconds.\n\n![Overview](docs/images/main-dash.png)\n\n### Tasks\n\nLive task list with saved views (All, Pending, Running, Failed, Needs retry, Dead-letter), filters by type / status / idempotency-token presence, full-text search by id / type / token, bulk retry / cancel / delete, and pagination. Clicking a row opens an inline drawer with tabs for payload, attempt history with error traces, timing breakdown, related tasks, the raw DB row, and a `curl` snippet to reproduce the call.\n\n![Tasks list](docs/images/tasks-list.png)\n\n![Task drawer](docs/images/task-detail.png)\n\n![Failed tasks](docs/images/dlq.png)\n\n### Workers\n\nShows the currently elected leader (with lease remaining + auto-renew countdown) and a live snapshot of every task in `running` status across the cluster, including current attempt and elapsed time.\n\n![Workers](docs/images/workers.png)\n\n### Maintenance\n\nStatus mix at a glance, the most recent failures, and a description of the leader-elected background jobs (Rescuer, Cleaner, Leader election) that run only on the elected leader.\n\n![Maintenance](docs/images/maintenance.png)\n\n### Authentication\n\n**Basic Auth:**\n\n```go\nui.HandlerOpts{\n    BasicAuth: \u0026ui.BasicAuth{Username: \"admin\", Password: \"secret\"},\n}\n```\n\n**OAuth providers** (e.g., GitHub):\n\n```go\nui.HandlerOpts{\n    AuthProviders: []ui.AuthProvider{githubProvider},\n    SecureCookies: true,    // for HTTPS\n    SessionMaxAge: 24*time.Hour,\n}\n```\n\nImplement the `ui.AuthProvider` interface to add any OAuth/SSO provider. Multiple providers can be configured in parallel — the login screen shows one button per provider. See [`examples/demo/github_provider.go`](https://github.com/yakser/asynqpg/blob/master/examples/demo/github_provider.go) for a complete GitHub OAuth implementation.\n\n![GitHub OAuth login](docs/images/github-oauth.png)\n\n![User profile](docs/images/profile.png)\n\n## Observability\n\nAll public components accept optional `MeterProvider` and `TracerProvider`. When nil, the global OpenTelemetry provider is used.\n\n### Metrics\n\n| Metric | Type | Description |\n|---|---|---|\n| `asynqpg.tasks.enqueued` | Counter | Tasks enqueued |\n| `asynqpg.tasks.processed` | Counter | Tasks finished processing |\n| `asynqpg.tasks.errors` | Counter | Processing or enqueue errors |\n| `asynqpg.task.duration` | Histogram | Handler execution duration (seconds) |\n| `asynqpg.task.enqueue_duration` | Histogram | Enqueue latency (seconds) |\n| `asynqpg.tasks.in_flight` | UpDownCounter | Currently executing tasks |\n\nAll metrics are tagged with `task_type` and `status` attributes.\n\n### Tracing\n\nSpans are created for all enqueue and processing operations with proper `SpanKind` (Producer/Consumer/Client).\n\n### Grafana Dashboard\n\nA pre-built Grafana dashboard is included at [`deploy/grafana/dashboards/asynqpg-overview.json`](deploy/grafana/dashboards/asynqpg-overview.json). It is provisioned automatically when you run `make demo-up`.\n\nThe dashboard refreshes every 10 seconds and includes:\n\n- **Stat panels** – total tasks enqueued, processed, errors, and currently in-flight\n- **Enqueue Rate** – tasks enqueued per second over time\n- **Processing Rate by Status** – throughput broken down by `completed` / `failed` / `cancelled`\n- **Task Duration Quantiles** – p50/p95/p99 handler execution latency\n- **Enqueue Duration Quantiles** – p50/p95/p99 enqueue latency\n- **Error Rate by Type** – error count per task type over time\n- **Error Ratio** – fraction of processed tasks that errored\n- **In-Flight Over Time** – concurrently executing tasks over time\n\nAll panels support a **Task Type** variable filter to drill down into a specific task type.\n\n### Observability Stack\n\nThe demo includes a full observability stack (Jaeger, Prometheus, Grafana, OTel Collector) in `deploy/`:\n\n```bash\nmake demo-up   # start PostgreSQL + observability stack\n```\n\n- **Jaeger** – `http://localhost:16686`\n- **Prometheus** – `http://localhost:9090`\n- **Grafana** – `http://localhost:3000`\n\n![Grafana dashboard](docs/images/grafana.png)\n\n![Jaeger traces](docs/images/jaeger-ui-all-traces.png)\n\n![Jaeger error trace](docs/images/jaeger-ui-error-trace.png)\n\n## Demo\n\nAn interactive TUI demo with producers, consumers, web UI, and observability.\n\n### Quick Start\n\n```bash\nmake demo       # start infra, migrate, run demo\nmake demo-down  # stop all services\n```\n\nOr run steps individually:\n\n```bash\nmake demo-up    # start PostgreSQL + observability stack\nmake migrate    # apply migrations\nmake demo-run   # run the demo app\n```\n\n### CLI Flags\n\nPass flags via `ARGS`:\n\n```bash\nmake demo-run ARGS=\"--tasks 50 --no-auto --log-level debug\"\n```\n\n| Flag | Default | Description |\n|---|---|---|\n| `--tasks, -n` | `100` | Number of initial tasks to seed |\n| `--no-auto` | `false` | Disable automatic task generation |\n| `--no-logs` | `false` | Hide the log viewport |\n| `--log-level, -l` | `info` | Log level: `debug`, `info`, `warn`, `error` |\n\n### TUI Commands\n\n| Command | Description |\n|---|---|\n| `enqueue \u003ctype\u003e [N]` | Enqueue N tasks (types: `email`, `notification`, `report`) |\n| `auto on\\|off` | Toggle automatic task generation |\n| `stats` | Show processing statistics |\n| `clear` | Clear log viewport |\n| `help` | Show available commands |\n| `quit` | Graceful shutdown |\n\nScroll logs with mouse wheel, arrow keys, or PgUp/PgDown. Press Tab to switch focus between input and log viewport.\n\n### Authentication\n\nThe demo supports optional authentication for the web UI via environment variables. Create a `.env` file in `examples/demo/` or pass variables directly:\n\n**Basic Auth:**\n\n```bash\nBASIC_AUTH_USER=admin BASIC_AUTH_PASS=secret make demo-run\n```\n\n**GitHub OAuth:**\n\n```bash\nGITHUB_CLIENT_ID=your_id GITHUB_CLIENT_SECRET=your_secret make demo-run\n```\n\nWithout these variables, the UI runs without authentication.\n\n## Performance\n\nThe library includes Go benchmarks covering SQL-level operations, batch completion, producer throughput, and consumer processing.\n\n```bash\nmake bench   # Run benchmarks (uses testcontainers, requires Docker)\n```\n\n## Testing\n\n```bash\nmake test              # unit tests (go test -race -count=1 ./...)\nmake test-integration  # integration tests (uses testcontainers, no manual DB needed)\nmake test-all          # both\nmake bench             # benchmarks (integration, requires Docker)\nmake lint              # golangci-lint\n```\n\n## Contributing\n\nContributions are welcome, including bug reports, feature requests,\ndocumentation improvements, and code changes. See [`CONTRIBUTING.md`](CONTRIBUTING.md)\nfor local setup, contribution workflow, testing expectations, and pull request\nguidelines.\n\n## Support\n\n- **Website**: [yakser.github.io/asynqpg-landing](https://yakser.github.io/asynqpg-landing)\n- **Bug reports \u0026 feature requests**: [GitHub Issues](https://github.com/yakser/asynqpg/issues)\n\n## TODO\n\n- [ ] Move `testutils` to a separate module to avoid pulling `testcontainers-go` into consumer projects\n\n## Project Status\n\nUnder active development. The API may change before v1.0. Bug reports and\nfeature requests are welcome.\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyakser%2Fasynqpg","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyakser%2Fasynqpg","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyakser%2Fasynqpg/lists"}