Background jobs (Asynq)

Enqueue, retry, schedule.

9 minmedium

Some work shouldn't block the HTTP request — sending an email, resizing an image, calling an LLM. Grit uses Asynq for background jobs: enqueue from anywhere, a worker process consumes them. This lesson covers the pattern + the four primitives you'll actually use.

How it's shaped

Producer (your handler/service)
→ jobs.Enqueue(ctx, task) puts JSON into Redis
returns immediately
Worker (separate Go process)
→ polls Redis, dequeues tasks
→ calls the registered handler for the task type
→ marks done / retries on error / moves to dead-letter

Two processes — the API and the worker — share Redis as the queue. Both run from the same binary; you start the worker with ./api worker instead of ./api server.

Defining a task

apps/api/internal/jobs/welcome_email.go
const TaskTypeWelcomeEmail = "user:welcome_email"
type WelcomeEmailPayload struct {
UserID string `json:"user_id"`
}
func (j *Jobs) EnqueueWelcomeEmail(ctx context.Context, userID string) error {
payload, _ := json.Marshal(WelcomeEmailPayload{UserID: userID})
_, err := j.client.EnqueueContext(ctx, asynq.NewTask(TaskTypeWelcomeEmail, payload),
asynq.Queue("default"),
asynq.MaxRetry(5),
)
return err
}

Handling the task in the worker

apps/api/internal/jobs/welcome_email_handler.go
func (j *Jobs) HandleWelcomeEmail(ctx context.Context, task *asynq.Task) error {
var p WelcomeEmailPayload
if err := json.Unmarshal(task.Payload(), &p); err != nil {
return fmt.Errorf("unmarshal: %w", err)
}
user, err := j.userService.GetByID(ctx, p.UserID)
if err != nil {
return fmt.Errorf("get user: %w", err)
}
return j.mailer.SendWelcome(ctx, user.Email, user.Name)
}

Return nil = task completed. Return an error = Asynq retries with exponential backoff up to MaxRetry, then moves it to the dead-letter queue.

Enqueuing from a handler

apps/api/internal/handlers/auth.go (excerpt)
// After register succeeds
if err := h.jobs.EnqueueWelcomeEmail(c.Request.Context(), user.ID); err != nil {
// Log + continue — registration succeeded; the welcome email is best-effort
log.Printf("welcome email enqueue: %v", err)
}
respond.Created(c, user, "Account created")

Notice — enqueue failure doesn't fail the request. The user's account exists; the welcome email is best-effort. Different behaviour for critical paths (payments, audit) where you'd return the error.

Scheduled tasks (cron)

For recurring work (daily digests, hourly metrics roll-up):

// In internal/jobs/scheduler.go
scheduler.Register("0 9 * * *", "daily:digest", nil) // 9 AM daily
scheduler.Register("0 * * * *", "metrics:rollup", nil) // hourly

Same handler signature as one-off tasks. The scheduler runs as part of the worker process — no separate cron daemon.

Don't run the worker inside the API process. If a long-running job hangs, it blocks an HTTP request slot. Always boot the worker separately: ./api worker on its own container/VM/systemd unit.

The Asynq admin UI

Grit's scaffold includes /admin/jobs in the admin app — visual dashboard for active/pending/failed/dead jobs, with retry + delete buttons. For the API-only kit, expose Asynq's web UI on a dedicated port; password-protected via the Sentinel dashboard credentials.

Quick check

A user signs up. You enqueue a welcome-email task. The worker is down for an hour. What happens to the task?

Try it

Wire your first job. Register a new task type:

  1. Define TaskTypeLogHello + a handler that just log.Println("hello from a job!").
  2. Add an enqueue call to your /api/health handler.
  3. Start the worker: go run ./cmd/server worker (or whatever the scaffolded subcommand is).
  4. Hit /api/health — you should see "hello from a job!" in the worker's output.

What's next

Transactional email is the most common job. Next lesson — how to send it via Resend and watch it land in Mailhog during dev.

Spot a typo? Have an idea?

Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled — suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.

Suggest an improvement on GitHub