Skip to content

Queues and scheduling

Every application has a queue — app.Queue() is never nil. Handlers are registered explicitly and payloads are typed JSON:

const TypeWelcomeEmail = "emails.welcome"
type WelcomePayload struct {
Email string `json:"email"`
}
queue.Register(app.Queue(), TypeWelcomeEmail,
func(ctx context.Context, payload WelcomePayload) error {
return mailer.Send(ctx, welcome(payload.Email))
})
err := queue.Dispatch(ctx, app.Queue(), TypeWelcomeEmail,
WelcomePayload{Email: email},
queue.Delay(time.Minute), queue.MaxRetry(5))

Scaffold a typed job with gin-kit generate job <Name> — it renders the payload struct, handler, and registration snippet (runtime project type only; generate event and generate mail do the same for typed events and mailables).

The default sync driver executes jobs inline — perfect for development and tests. Set QUEUE_DRIVER=redis (with REDIS_URL) for production: jobs become persistent asynq tasks with exponential-backoff retries, delayed execution, named queues, and QUEUE_CONCURRENCY worker goroutines. The worker runs as a supervised runner inside your application process, drains in-flight jobs on shutdown, and contributes a redis readiness check. For dedicated worker binaries, call Queue.Start(ctx) yourself.

runtime/schedule wraps robfig/cron with panic recovery and graceful stop:

scheduler := schedule.New(schedule.Options{Logger: app.Logger()})
scheduler.Cron("*/5 * * * *", "sync-inventory", syncInventory)
scheduler.Every(30*time.Second, "heartbeat", heartbeat)
scheduler.Daily("prune-sessions", pruneSessions, schedule.SkipIfRunning())
app.Go("scheduler", scheduler.Run)

A failing or panicking job is logged and never stops the scheduler or the application. SkipIfRunning prevents overlapping runs of slow jobs.

The scheduler is single-instance: running N replicas executes every job N times. Run it in one instance or a dedicated binary when scaling out.

Both the queue worker and the scheduler build on app.Go(name, fn): any long-running goroutine registered this way shares the application’s lifecycle — it receives cancellation on shutdown, and its failure triggers a graceful shutdown instead of a silent hang.

For tests and short-lived binaries, app.Close(ctx) runs shutdown hooks exactly once without serving HTTP — apptest.New calls it automatically via t.Cleanup.