<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Chizee</title>
    <description>The latest articles on DEV Community by Chizee (@chizee).</description>
    <link>https://dev.to/chizee</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4066137%2Fea432cc9-6883-4e71-afc0-40c7345c5ead.png</url>
      <title>DEV Community: Chizee</title>
      <link>https://dev.to/chizee</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chizee"/>
    <language>en</language>
    <item>
      <title>I Replaced Redis and RabbitMQ With 15 Lines of Postgres</title>
      <dc:creator>Chizee</dc:creator>
      <pubDate>Sun, 09 Aug 2026 13:01:00 +0000</pubDate>
      <link>https://dev.to/chizee/i-replaced-redis-and-rabbitmq-with-15-lines-of-postgres-3266</link>
      <guid>https://dev.to/chizee/i-replaced-redis-and-rabbitmq-with-15-lines-of-postgres-3266</guid>
      <description>&lt;h1&gt;
  
  
  Launch Workspace — "Just Use Postgres" Week 1
&lt;/h1&gt;

&lt;p&gt;Route: &lt;strong&gt;TBD&lt;/strong&gt; (A = canonical build on omenabyte.com → dev.to API · B = dev.to-first · C = draft-only manual publish)&lt;br&gt;
Decision made: &lt;em&gt;pending Boss's pick&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  ARTICLE 1 — dev.to-optimized draft
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Title:&lt;/strong&gt; I Replaced Redis and RabbitMQ With 15 Lines of Postgres&lt;br&gt;
&lt;strong&gt;Tags:&lt;/strong&gt; postgres, database, backend, tutorial&lt;br&gt;
&lt;strong&gt;Cover image:&lt;/strong&gt; &lt;code&gt;Just-Use-Postgres-Field-Manual-cover.png&lt;/code&gt; (blueprint elephant, navy/amber — matches dev.to dark theme)&lt;br&gt;
&lt;strong&gt;Canonical URL (route A):&lt;/strong&gt; &lt;code&gt;https://omenabyte.com/blog/replace-redis-rabbitmq-with-postgres&lt;/code&gt;&lt;/p&gt;



&lt;p&gt;Every background job system I've built started the same way: &lt;em&gt;"we'll just add Redis for the queue."&lt;/em&gt; Then six months later there's a broker to patch, secure, monitor, and pay for — doing a job Postgres can do with one SQL clause.&lt;/p&gt;

&lt;p&gt;That clause is &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt;, and once you've used it, a message broker feels like overkill for most job-queue workloads.&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem with a naive SQL queue
&lt;/h2&gt;

&lt;p&gt;The reason people avoid building a queue directly on a table is real: two workers can grab the same "pending" row at the same time, one locks it, and the other sits there waiting. That's a legitimate deadlock risk — if you build it naively.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix: SKIP LOCKED
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SKIP LOCKED&lt;/code&gt; tells Postgres: if a row is already locked by another transaction, don't wait for it — skip straight to the next one. That single behavior turns an ordinary table into a safe, concurrent queue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt;     &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;locked_at&lt;/span&gt;   &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;  &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Without this, the query below does a full table scan on every poll&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_jobs_status_created&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'processing'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the actual dequeue query every worker runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;next_job&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;
     &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'processing'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;locked_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'5 minutes'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;LOCKED&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'processing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locked_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;next_job&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next_job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;locked_at&lt;/code&gt; check matters more than it looks. It's the equivalent of a message queue's &lt;em&gt;visibility timeout&lt;/em&gt; — if a worker crashes mid-job, the job doesn't stay stuck in &lt;code&gt;processing&lt;/code&gt; forever. Another worker reclaims it after five minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  I actually tested this for duplicates
&lt;/h2&gt;

&lt;p&gt;I ran this exact query five times in a row against a seeded table: three fresh jobs and one simulated crashed job. Every job came back exactly once, in order, and the crashed job was correctly reclaimed on the fourth call. Zero duplicates, zero races.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this beats adding Redis for most teams
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No broker to operate.&lt;/strong&gt; Nothing new to patch, secure, or monitor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job history lives with your data.&lt;/strong&gt; You can join a job row straight to the order or user it belongs to — try doing that across two databases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crashes don't lose work.&lt;/strong&gt; The reclaim logic above handles it natively.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where Redis still wins
&lt;/h2&gt;

&lt;p&gt;If you need sub-millisecond pub/sub fan-out to thousands of concurrent WebSocket clients, or a pure in-memory cache absorbing extreme read traffic, that's a different problem — Redis is still the right tool there. But &lt;em&gt;"give my background jobs somewhere safe to live"&lt;/em&gt; almost never needs a separate service.&lt;/p&gt;




&lt;p&gt;This is one of eight infrastructure swaps in &lt;strong&gt;Just Use Postgres&lt;/strong&gt;, a 24-page field manual on replacing MongoDB, Redis, Elasticsearch, Pinecone, and more with the database you're probably already running. Every recipe in it — including this one — was run against a live Postgres instance before it went in the book.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Get the field manual&lt;/strong&gt; — launch price &lt;strong&gt;$14&lt;/strong&gt; instead of walking into a $50/month managed-Redis invoice: &lt;a href="https://payhip.com/omenabyte" rel="noopener noreferrer"&gt;https://payhip.com/omenabyte&lt;/a&gt;&lt;br&gt;
🐳 Or take the &lt;strong&gt;$24 bundle&lt;/strong&gt; with the full &lt;code&gt;docker-compose up&lt;/code&gt; starter repo — all 8 modules as tested, runnable migrations + seed data: &lt;a href="https://4693433176360.gumroad.com/" rel="noopener noreferrer"&gt;https://4693433176360.gumroad.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Read this on omenabyte.com → &lt;a href="https://omenabyte.com/blog/replace-redis-rabbitmq-with-postgres" rel="noopener noreferrer"&gt;https://omenabyte.com/blog/replace-redis-rabbitmq-with-postgres&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>backend</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
