<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ timothy ogbemudia - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ timothy ogbemudia - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Wed, 10 Jun 2026 16:19:17 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/glamboyosa/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a PostgreSQL-Backed Job Queue in Go ]]>
                </title>
                <description>
                    <![CDATA[ When you build a web application, not every task should happen inside a user's request. Some work is slow. Some work can fail. Some work should happen later. Sending emails, resizing images, processin ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-postgresql-backed-job-queue-in-go/</link>
                <guid isPermaLink="false">6a28a0135ea1e6904efb11dc</guid>
                
                    <category>
                        <![CDATA[ PostgreSQL ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Queues ]]>
                    </category>
                
                    <category>
                        <![CDATA[ golang ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ timothy ogbemudia ]]>
                </dc:creator>
                <pubDate>Tue, 09 Jun 2026 23:21:55 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/f16f87ae-8900-40e9-ba3b-64bf50cc1fe1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When you build a web application, not every task should happen inside a user's request.</p>
<p>Some work is slow. Some work can fail. Some work should happen later. Sending emails, resizing images, processing webhooks, generating reports, and retrying third-party APIs are all good examples.</p>
<p>These tasks are usually handled by a background job system.</p>
<p>In this article, you'll use an open source Go project called <a href="https://github.com/glamboyosa/swig">Swig</a> as a practical example of how a PostgreSQL-backed job queue works in practice.</p>
<p>By the end, you'll understand how to build a background job queue with Go and PostgreSQL, and why PostgreSQL is more capable than most developers realize.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-you-will-learn">What You Will Learn</a></p>
</li>
<li><p><a href="#heading-what-is-a-job-queue">What Is a Job Queue?</a></p>
</li>
<li><p><a href="#heading-why-use-postgresql-for-a-queue">Why Use PostgreSQL for a Queue?</a></p>
</li>
<li><p><a href="#heading-swigs-architecture">Swig's Architecture</a></p>
</li>
<li><p><a href="#heading-how-to-represent-jobs-in-postgresql">How to Represent Jobs in PostgreSQL</a></p>
</li>
<li><p><a href="#heading-how-to-define-a-worker-in-go">How to Define a Worker in Go</a></p>
</li>
<li><p><a href="#heading-how-to-register-workers-without-sharing-state">How to Register Workers Without Sharing State</a></p>
</li>
<li><p><a href="#heading-how-to-add-a-job">How to Add a Job</a></p>
</li>
<li><p><a href="#heading-how-to-handle-multiple-workers-safely">How to Handle Multiple Workers Safely</a></p>
</li>
<li><p><a href="#heading-how-to-use-goroutines-for-concurrent-workers">How to Use Goroutines for Concurrent Workers</a></p>
</li>
<li><p><a href="#heading-how-to-wake-workers-with-listennotify">How to Wake Workers with LISTEN/NOTIFY</a></p>
</li>
<li><p><a href="#heading-how-to-elect-a-leader-with-advisory-locks">How to Elect a Leader with Advisory Locks</a></p>
</li>
<li><p><a href="#heading-how-to-handle-failed-jobs">How to Handle Failed Jobs</a></p>
</li>
<li><p><a href="#heading-how-to-abstract-the-database-driver">How to Abstract the Database Driver</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along, you should have:</p>
<ul>
<li><p>Basic familiarity with Go (structs, interfaces, goroutines)</p>
</li>
<li><p>A working understanding of PostgreSQL and SQL</p>
</li>
<li><p>Go installed (1.21 or later)</p>
</li>
<li><p>A PostgreSQL instance available locally or remotely</p>
</li>
</ul>
<h2 id="heading-what-you-will-learn">What You Will Learn</h2>
<ul>
<li><p>How to represent and store jobs in PostgreSQL</p>
</li>
<li><p>How to claim jobs safely across concurrent workers using <code>FOR UPDATE SKIP LOCKED</code></p>
</li>
<li><p>How to wake workers efficiently using <code>LISTEN/NOTIFY</code></p>
</li>
<li><p>How to elect a leader across instances using advisory locks</p>
</li>
<li><p>How Go interfaces, goroutines, contexts, and transactions fit together in a real system</p>
</li>
</ul>
<h2 id="heading-what-is-a-job-queue">What Is a Job Queue?</h2>
<p>A job queue is a system that stores work to be done later.</p>
<p>Your application adds a job to the queue. A worker takes a job from the queue and runs it.</p>
<p>For example, when a user signs up, your application might create the user immediately and then add a job like this:</p>
<pre><code class="language-json">{
  "kind": "send_welcome_email",
  "payload": {
    "to": "user@example.com",
    "subject": "Welcome!"
  }
}
</code></pre>
<p>A background worker later picks up that job and sends the email. This keeps the user request fast. The signup route doesn't need to wait for the email provider before returning a response.</p>
<p>A job queue usually needs to answer a few important questions:</p>
<ul>
<li><p>Where are jobs stored?</p>
</li>
<li><p>How do workers find jobs?</p>
</li>
<li><p>How do you stop two workers from processing the same job?</p>
</li>
<li><p>How do you retry failed jobs?</p>
</li>
<li><p>How do you shut workers down safely?</p>
</li>
<li><p>How do you keep job creation consistent with application data?</p>
</li>
</ul>
<p>Swig answers those questions with Go and PostgreSQL.</p>
<h2 id="heading-why-use-postgresql-for-a-queue">Why Use PostgreSQL for a Queue?</h2>
<p>Many job queues use Redis, RabbitMQ, SQS, or Kafka. Those are all useful tools. But many applications already depend on PostgreSQL. If your app already has Postgres, you may not want to operate another service just to run background jobs.</p>
<p>PostgreSQL gives you several features that are surprisingly useful for queues:</p>
<ul>
<li><p>Tables for durable job storage</p>
</li>
<li><p>Transactions for atomic writes</p>
</li>
<li><p>Row locks for safe concurrent processing</p>
</li>
<li><p><code>SKIP LOCKED</code> for letting workers claim different jobs</p>
</li>
<li><p><code>LISTEN/NOTIFY</code> for waking workers when new jobs arrive</p>
</li>
<li><p>Advisory locks for leader election</p>
</li>
<li><p>JSONB for flexible job payloads</p>
</li>
</ul>
<p>The tradeoff is important. A PostgreSQL-backed queue isn't trying to replace Kafka for event streaming or RabbitMQ for complex routing. It makes common application background jobs simple, reliable, and easy to operate without adding infrastructure.</p>
<h2 id="heading-swigs-architecture">Swig's Architecture</h2>
<p>At a high level, Swig has five parts:</p>
<ol>
<li><p>A <code>swig_jobs</code> table in PostgreSQL</p>
</li>
<li><p>Go workers that process jobs</p>
</li>
<li><p>A worker registry that maps job names to worker types</p>
</li>
<li><p>A driver layer that supports both <code>pgx</code> and <code>database/sql</code></p>
</li>
<li><p>A leader loop for shared maintenance work</p>
</li>
</ol>
<p>The basic flow looks like this:</p>
<ol>
<li><p>Your app calls <code>AddJob</code></p>
</li>
<li><p>Swig serializes the job payload to JSON</p>
</li>
<li><p>Swig inserts a row into <code>swig_jobs</code></p>
</li>
<li><p>PostgreSQL sends a notification that a job was created</p>
</li>
<li><p>A Go worker wakes up and tries to claim one pending job</p>
</li>
<li><p>PostgreSQL row locks ensure only one worker claims that row</p>
</li>
<li><p>The worker runs the job</p>
</li>
<li><p>Swig marks the job as completed or failed</p>
</li>
</ol>
<p>The hard parts are concurrency, failure, connection lifecycle, and shutdown. That's where Go and PostgreSQL work together.</p>
<h2 id="heading-how-to-represent-jobs-in-postgresql">How to Represent Jobs in PostgreSQL</h2>
<p>A simplified version of Swig's job table looks like this:</p>
<pre><code class="language-sql">CREATE TABLE swig_jobs (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  kind TEXT NOT NULL,
  queue TEXT NOT NULL,
  payload JSONB NOT NULL,
  status TEXT NOT NULL DEFAULT 'pending',
  priority INTEGER NOT NULL DEFAULT 0,
  attempts INTEGER NOT NULL DEFAULT 0,
  max_attempts INTEGER NOT NULL DEFAULT 3,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  scheduled_for TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  instance_id UUID,
  worker_id UUID,
  locked_at TIMESTAMPTZ,
  last_error TEXT,
  last_error_at TIMESTAMPTZ
);
</code></pre>
<p>Each row is one job. The important columns are:</p>
<ul>
<li><p><code>kind</code>: the type of job, such as <code>send_email</code></p>
</li>
<li><p><code>payload</code>: the JSON data needed to run the job</p>
</li>
<li><p><code>status</code>: whether the job is pending, processing, completed, or failed</p>
</li>
<li><p><code>attempts</code>: how many times the job has been tried</p>
</li>
<li><p><code>scheduled_for</code>: when the job is allowed to run</p>
</li>
<li><p><code>locked_at</code>: when the job was claimed</p>
</li>
</ul>
<p>The table is the source of truth. PostgreSQL notifications can wake workers, but notifications aren't the durable queue. The rows in <code>swig_jobs</code> are.</p>
<h2 id="heading-how-to-define-a-worker-in-go">How to Define a Worker in Go</h2>
<p>In Swig, a worker is a Go type that knows how to process one kind of job.</p>
<p>Here's a simple email worker:</p>
<pre><code class="language-go">type EmailWorker struct {
    To      string `json:"to"`
    Subject string `json:"subject"`
    Body    string `json:"body"`
}

func (w *EmailWorker) JobName() string {
    return "send_email"
}

func (w *EmailWorker) Process(ctx context.Context) error {
    fmt.Printf("Sending email to %s with subject %s\n", w.To, w.Subject)
    return nil
}
</code></pre>
<p>There are two important methods:</p>
<ul>
<li><p><code>JobName</code> tells Swig what kind of job this worker handles</p>
</li>
<li><p><code>Process</code> contains the actual work</p>
</li>
</ul>
<p>The struct fields are also the job arguments. When you enqueue an <code>EmailWorker</code>, Swig serializes the struct into JSON and stores it in PostgreSQL. Later, a worker claims the row, unmarshals the JSON back into a fresh <code>EmailWorker</code>, and calls <code>Process</code>.</p>
<h3 id="heading-go-interfaces">Go Interfaces</h3>
<p>Go interfaces describe behavior. Swig doesn't need to know the exact concrete type of every worker. It only needs to know that a worker can provide a job name and process a job:</p>
<pre><code class="language-go">type Worker interface {
    JobName() string
    Process(context.Context) error
}
</code></pre>
<p>If a type has those methods, it satisfies the interface with no explicit declaration required. This is one of the reasons interfaces are so useful in Go. They let you design around behavior instead of inheritance.</p>
<h2 id="heading-how-to-register-workers-without-sharing-state">How to Register Workers Without Sharing State</h2>
<p>Swig has a worker registry that maps a job name to a worker type:</p>
<pre><code class="language-go">registry := workers.NewWorkerRegistry()
registry.RegisterWorker(&amp;EmailWorker{})
</code></pre>
<p>Later, when a job row says <code>kind = 'send_email'</code>, Swig looks up the registered worker and runs it.</p>
<p>There's a subtle concurrency issue here. If the registry stored the exact <code>&amp;EmailWorker{}</code> pointer and reused it for every job, multiple goroutines could unmarshal payloads into the same Go value at the same time.</p>
<p>Swig avoids this with a factory approach internally. Registration captures the worker type, and each claimed job gets a fresh worker instance before JSON is unmarshaled. The API stays simple, but internally Swig creates a new <code>EmailWorker</code> for each job. This is a useful Go pattern: keep the public API simple while making the internal lifecycle safer.</p>
<h2 id="heading-how-to-add-a-job">How to Add a Job</h2>
<p>Here's what adding a job looks like from the user side:</p>
<pre><code class="language-go">err := swigClient.AddJob(ctx, &amp;EmailWorker{
    To:      "user@example.com",
    Subject: "Welcome!",
    Body:    "Thanks for signing up.",
})
</code></pre>
<p>Inside Swig, the process is roughly:</p>
<pre><code class="language-go">argsJSON, err := json.Marshal(workerWithArgs)
if err != nil {
    return err
}

_, err = db.ExecContext(ctx, `
    INSERT INTO swig_jobs (kind, queue, payload, priority, scheduled_for, status)
    VALUES (\(1, \)2, \(3, \)4, $5, 'pending')
`, jobName, queue, argsJSON, priority, runAt)
</code></pre>
<h3 id="heading-how-to-enqueue-jobs-inside-transactions">How to Enqueue Jobs Inside Transactions</h3>
<p>One of the best reasons to use PostgreSQL for jobs is transactional enqueueing.</p>
<p>Imagine a user signs up. You want to insert the user and queue a welcome email. If those happen separately, you can get inconsistent states. With a transaction, both succeed or both fail:</p>
<pre><code class="language-go">tx, err := pool.Begin(ctx)
if err != nil {
    return err
}
defer tx.Rollback(ctx)

_, err = tx.Exec(ctx, `INSERT INTO users (email) VALUES ($1)`, email)
if err != nil {
    return err
}

err = swigClient.AddJobWithTx(ctx, tx, &amp;EmailWorker{
    To:      email,
    Subject: "Welcome!",
    Body:    "Thanks for joining.",
})
if err != nil {
    return err
}

return tx.Commit(ctx)
</code></pre>
<p>If the transaction rolls back, the user isn't created and the job isn't queued. This is much harder to guarantee when your database and queue are separate systems.</p>
<h2 id="heading-how-to-handle-multiple-workers-safely">How to Handle Multiple Workers Safely</h2>
<p>A queue gets interesting when many workers run at the same time. Imagine three workers all asking PostgreSQL for the next pending job. You don't want all three to process the same job.</p>
<p>A naïve approach has a race condition. Two workers can select the same job before either one updates it.</p>
<h3 id="heading-postgresql-for-update-skip-locked">PostgreSQL FOR UPDATE SKIP LOCKED</h3>
<p>PostgreSQL can lock rows selected inside a transaction. <code>FOR UPDATE</code> means "lock this row because I plan to update it." <code>SKIP LOCKED</code> means "if another worker already locked a row, skip it and find another one."</p>
<p>This is perfect for a queue:</p>
<ul>
<li><p>Worker A locks job 1</p>
</li>
<li><p>Worker B skips job 1 and locks job 2</p>
</li>
<li><p>Worker C skips jobs 1 and 2 and locks job 3</p>
</li>
</ul>
<p>No central coordinator is needed. Swig uses an atomic update pattern:</p>
<pre><code class="language-sql">UPDATE swig_jobs
SET status = 'processing',
    instance_id = $1,
    worker_id = $2,
    locked_at = NOW(),
    attempts = attempts + 1
WHERE id = (
  SELECT id
  FROM swig_jobs
  WHERE status = 'pending'
    AND scheduled_for &lt;= NOW()
  ORDER BY priority DESC, created_at
  FOR UPDATE SKIP LOCKED
  LIMIT 1
)
RETURNING id, kind, payload;
</code></pre>
<p>This query finds a pending job, skips already-locked jobs, marks it as processing, records which worker claimed it, and returns the job data. All of this happens atomically. Workers never do a separate <code>SELECT</code> and hope the later <code>UPDATE</code> is still safe.</p>
<h2 id="heading-how-to-use-goroutines-for-concurrent-workers">How to Use Goroutines for Concurrent Workers</h2>
<p>Swig starts worker loops as goroutines:</p>
<pre><code class="language-go">for i := 0; i &lt; maxWorkers; i++ {
    go s.startWorker(ctx, queueType)
}
</code></pre>
<p>Each worker runs independently. PostgreSQL coordinates which job each worker gets. Go handles concurrency with goroutines, while PostgreSQL handles safe job claiming with locks.</p>
<h3 id="heading-how-to-handle-graceful-shutdown">How to Handle Graceful Shutdown</h3>
<p>When a service shuts down, it should wait for workers to finish cleanly. Go's <code>sync.WaitGroup</code> helps:</p>
<pre><code class="language-go">var wg sync.WaitGroup

wg.Add(1)
go func() {
    defer wg.Done()
    processJobs()
}()

wg.Wait()
</code></pre>
<p>Swig also uses <code>sync.Once</code> to make shutdown idempotent. Calling <code>Stop</code> more than once shouldn't panic because of a double channel close. Shutdown paths are often where production systems behave differently from happy-path demos.</p>
<h2 id="heading-how-to-wake-workers-with-listennotify">How to Wake Workers with LISTEN/NOTIFY</h2>
<p>If workers constantly poll the database for jobs, they waste resources when the queue is empty. PostgreSQL has <code>LISTEN/NOTIFY</code> to solve this.</p>
<p>A connection can listen on a channel:</p>
<pre><code class="language-sql">LISTEN swig_jobs;
</code></pre>
<p>Another session can send a notification:</p>
<pre><code class="language-sql">NOTIFY swig_jobs, '{"id":"job-id"}';
</code></pre>
<p>Swig creates a trigger so PostgreSQL sends a notification after a job is inserted. Workers sleep when there's no work and wake when a new job arrives.</p>
<p>There's an important PostgreSQL detail here: <code>LISTEN</code> is session-scoped. A worker must wait for notifications on the same database session that executed <code>LISTEN</code>. Swig handles this by creating a dedicated listener for each worker that owns one database session throughout its lifecycle.</p>
<p>This is a common backend engineering lesson: abstractions like connection pools are useful, but some database features depend on the lifecycle of a specific connection.</p>
<h2 id="heading-how-to-elect-a-leader-with-advisory-locks">How to Elect a Leader with Advisory Locks</h2>
<p>Some queue maintenance tasks should only run on one instance at a time, including retrying failed jobs, recovering stale jobs, and cleaning old history.</p>
<p>Swig uses PostgreSQL advisory locks for this:</p>
<pre><code class="language-sql">SELECT pg_try_advisory_lock($1);
</code></pre>
<p>If the result is true, that Swig instance becomes the leader. Advisory locks are also session-scoped, so Swig uses a dedicated advisory-lock connection for leadership. If that session ends, PostgreSQL releases the lock and another instance can take over. Simple failover without ZooKeeper or etcd.</p>
<h2 id="heading-how-to-handle-failed-jobs">How to Handle Failed Jobs</h2>
<p>When a worker returns an error, Swig records the error and either retries the job or marks it as failed:</p>
<pre><code class="language-sql">UPDATE swig_jobs
SET status = CASE
    WHEN attempts &gt;= max_attempts THEN 'failed'
    ELSE 'pending'
  END,
  last_error = $2,
  last_error_at = NOW()
WHERE id = $1;
</code></pre>
<h3 id="heading-a-note-on-delivery-semantics">A Note on Delivery Semantics</h3>
<p>It's tempting to say a job queue processes jobs exactly once. In distributed systems, that's a dangerous claim.</p>
<p>Consider this scenario:</p>
<ol>
<li><p>A worker sends an email</p>
</li>
<li><p>The worker crashes before marking the job completed</p>
</li>
<li><p>The job is retried</p>
</li>
<li><p>The email might be sent again</p>
</li>
</ol>
<p>The accurate description is that Swig provides atomic claiming and at-least-once processing. Because jobs can be retried, workers should be idempotent. Running the same operation more than once should produce the same result as running it once.</p>
<h2 id="heading-how-to-abstract-the-database-driver">How to Abstract the Database Driver</h2>
<p>Swig supports both <code>pgx</code> and <code>database/sql</code> through a driver interface:</p>
<pre><code class="language-go">type Driver interface {
    Exec(ctx context.Context, sql string, args ...interface{}) error
    Query(ctx context.Context, sql string, args ...interface{}) (Rows, error)
    QueryRow(ctx context.Context, sql string, args ...interface{}) Row
    WithTx(ctx context.Context, fn func(tx Transaction) error) error
    NewListener(ctx context.Context, channel string) (Listener, error)
    TryAdvisoryLock(ctx context.Context, lockID int64) (AdvisoryLock, bool, error)
}
</code></pre>
<p>The core queue code only depends on behavior, not a specific library. This is a common Go design: define the behavior your core package needs, write small adapters for concrete dependencies, and keep the core logic independent.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>A PostgreSQL-backed queue isn't the right answer for every system. If you need massive event streaming, Kafka may be a better fit. If you need complex routing, RabbitMQ may be better.</p>
<p>But for many Go applications, PostgreSQL is already there. Swig shows how far you can get with a small Go API and a few PostgreSQL features:</p>
<ul>
<li><p>Store jobs in a table</p>
</li>
<li><p>Claim jobs atomically with <code>FOR UPDATE SKIP LOCKED</code></p>
</li>
<li><p>Wake workers with dedicated <code>LISTEN/NOTIFY</code> sessions</p>
</li>
<li><p>Coordinate leadership with advisory locks</p>
</li>
<li><p>Keep app data and jobs consistent with transactions</p>
</li>
<li><p>Manage worker lifecycles with goroutines and contexts</p>
</li>
</ul>
<p>That combination makes a solid foundation for background processing and a great project for learning how Go and PostgreSQL work together in production systems. You can explore the full source code at <a href="https://github.com/glamboyosa/swig">github.com/glamboyosa/swig</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
