In modern microservice stacks, one need shows up fast: background job queues. The moment an app has to send emails asynchronously, process files, generate PDFs, recompute reports, or run scheduled tasks without blocking HTTP requests, teams tend to reach for Redis, RabbitMQ, or Kafka — and with that, more infrastructure to operate.

Kool Queue takes the opposite approach: it’s an open-source utility for Micronaut that proposes something deliberately simple — use PostgreSQL as the queue backend, so you can avoid external dependencies. The project leans on a well-known database pattern: store jobs in a table and distribute them across multiple workers using non-blocking row locking via FOR UPDATE SKIP LOCKED.

What Kool Queue is — and why it’s interesting

Kool Queue positions itself as a DB-based queue backend for Micronaut, designed around simplicity and throughput, with one key technical decision: PostgreSQL + FOR UPDATE SKIP LOCKED so multiple workers can “pull” pending jobs without waiting on locks.

That’s a very real-world proposition: PostgreSQL is already in production, you want async execution, and you’d rather not introduce another moving part just to get a queue.

This isn’t “the universal queue for everything.” It’s a pragmatic fit for a specific class of problems — the same way Rails’ Solid Queue popularized the idea of database-backed jobs in another ecosystem.

The core mechanic: FOR UPDATE SKIP LOCKED

SKIP LOCKED means that if one worker has already locked a row (a job), other workers will ignore it and move on, instead of waiting. In the job-queue context, the common pattern is:

  1. Select a pending job
  2. Lock it (so no other worker can grab it)
  3. Mark it IN_PROGRESS
  4. Execute the work
  5. Mark it DONE or ERROR

Kool Queue describes this lifecycle explicitly, with states like PENDING → IN_PROGRESS → DONE/ERROR, and a scheduler that polls for work.

Installation and minimal dependencies

At the stack level, Kool Queue expects a typical Micronaut + JPA + PostgreSQL setup:

  • Micronaut Data (Hibernate JPA)
  • HikariCP
  • PostgreSQL JDBC driver

Example (Kotlin/Gradle) starting point:

dependencies {
  implementation("io.micronaut.data:micronaut-data-hibernate-jpa")
  implementation("io.micronaut.sql:micronaut-jdbc-hikari")
  runtimeOnly("org.postgresql:postgresql")

  // Kool Queue (example; confirm the latest version you plan to use)
  implementation("com.github.joaquindiez:micronaut-kool-queue:0.2.1")
}
Code language: JavaScript (javascript)

A typical application.yml (adapt as needed):

datasources:
  default:
    url: jdbc:postgresql://localhost:5432/your_db
    username: your_username
    password: your_password
    driver-class-name: org.postgresql.Driver

jpa:
  default:
    properties:
      hibernate:
        hbm2ddl:
          auto: update

micronaut:
  scheduler:
    kool-queue:
      enabled: true
      max-concurrent-tasks: 3
      default-interval: 30s
      default-initial-delay: 10s
      shutdown-timeout-seconds: 30

Usage: define a job and enqueue it “later”

The library’s mental model is straightforward: you define a job class, implement process(...), and enqueue work via something like processLater(...).

Example (Kotlin):

import jakarta.inject.Singleton

data class EmailData(val recipient: String, val subject: String, val body: String)

@Singleton
class EmailNotificationJob : ApplicationJob<EmailData>() {

  override fun process(data: EmailData): Result<Boolean> {
    return try {
      // Real logic: send an email, call an API, etc.
      println("Sending email to ${data.recipient}: ${data.subject}")
      Result.success(true)
    } catch (e: Exception) {
      Result.failure(e)
    }
  }
}
Code language: HTML, XML (xml)

From a controller:

import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.*

@Controller("/notifications")
class NotificationController(private val emailJob: EmailNotificationJob) {

  @Post("/send-email")
  fun sendEmail(@Body emailData: EmailData): HttpResponse<String> {
    val ref = emailJob.processLater(emailData)
    return HttpResponse.ok("Email queued. Job ID: ${ref.jobId}")
  }
}
Code language: HTML, XML (xml)

Operationally, the scheduler handles the rest: polling, honoring max concurrency, and updating status as jobs progress.

Where it fits — and where it doesn’t

A database-backed queue can be “more than enough” when the goal is to reduce operational complexity. But the boundaries matter.

ApproachExtra infraBest atTypical riskWhen it’s a good choice
Kool Queue (PostgreSQL)NoFewer components; fast adoptionTable growth; DB tuning; contentionInternal jobs, moderate throughput, minimalist stack
Redis + workersYes (Redis)Very low latency; mature patternsRunning Redis + persistence concernsHigh job volume, low latency needs, easy horizontal scaling
RabbitMQYesRouting, ACK/NACK, classic messagingOperational + design overheadEnterprise messaging and routed workflows
KafkaYesStreaming, retention, replayComplexity and costEvent pipelines, auditing, data streaming
Solid Queue (Rails)NoProven patterns in Rails appsSimilar DB-queue limitsRails apps avoiding Redis

Sysadmin notes: how to keep it from becoming a problem later

If PostgreSQL is your queue, success is as much ops as it is code:

  • Indexes matter: once the jobs table grows, you’ll want strong indexing on status/queue/scheduled time.
  • Vacuum & bloat: status updates generate churn — watch autovacuum and table bloat.
  • Transaction duration: long-running locks can still hurt throughput even with SKIP LOCKED.
  • Observability: track backlog (PENDING), error rates (ERROR), and time spent IN_PROGRESS.
  • Retries/backoff: define policies (max retries, delays, a “dead letter” equivalent if needed).

Kool Queue doesn’t pretend to erase these realities — it just offers a clean, direct path to “queues now,” using infrastructure you already run.


FAQs

How can I add background job processing in Micronaut without Redis?
By using a database-backed approach like Kool Queue, storing jobs in PostgreSQL and distributing work using FOR UPDATE SKIP LOCKED.

What are the main benefits of using PostgreSQL as a queue?
Less infrastructure to operate, simpler deployments, and fewer failure points — especially if PostgreSQL is already part of your production stack.

Does FOR UPDATE SKIP LOCKED help multiple workers run in parallel?
Yes. It lets each worker ignore rows already locked by others, reducing lock waits and improving concurrency under load.

When should I move from a DB queue to RabbitMQ/Kafka/Redis?
When you need very low latency at high volume, advanced routing semantics, retention/replay, strict workload isolation, or more specialized delivery guarantees.

Scroll to Top