<?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: Rowan Feld</title>
    <description>The latest articles on DEV Community by Rowan Feld (@rowan_feld).</description>
    <link>https://dev.to/rowan_feld</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%2F4215034%2Ff537b518-859f-4110-9824-e35665a81bf0.jpg</url>
      <title>DEV Community: Rowan Feld</title>
      <link>https://dev.to/rowan_feld</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rowan_feld"/>
    <language>en</language>
    <item>
      <title>Queues: at-least-once means never exactly-once</title>
      <dc:creator>Rowan Feld</dc:creator>
      <pubDate>Sun, 01 Nov 2026 15:11:42 +0000</pubDate>
      <link>https://dev.to/rowan_feld/queues-at-least-once-means-never-exactly-once-5ed2</link>
      <guid>https://dev.to/rowan_feld/queues-at-least-once-means-never-exactly-once-5ed2</guid>
      <description>&lt;p&gt;The most useful sentence I've read about queues: at-least-once delivery is easy, exactly-once processing is a property of your handler, not the broker.&lt;/p&gt;

&lt;p&gt;Which means every consumer needs to survive a duplicate. The ways I do that, in order of preference:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Idempotent writes.&lt;/strong&gt; &lt;code&gt;UPDATE ... WHERE version = expected&lt;/code&gt; instead of blind writes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A dedupe key with a unique index.&lt;/strong&gt; Cheapest and most explicit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A processed table with a TTL&lt;/strong&gt;, for handlers with side effects.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The failure mode to design for isn't the duplicate, it's the poison message: one bad event that blocks the partition because the handler retries forever. Give every consumer a max attempt count and a dead-letter queue you actually read.&lt;/p&gt;

&lt;p&gt;One more: make the retry delay grow. A flat 1-second retry turns a small outage into a stampede.&lt;/p&gt;

&lt;p&gt;What's your dead-letter policy?&lt;/p&gt;

</description>
      <category>go</category>
      <category>backend</category>
      <category>queues</category>
    </item>
    <item>
      <title>A job queue in Postgres, two months later</title>
      <dc:creator>Rowan Feld</dc:creator>
      <pubDate>Thu, 29 Oct 2026 10:42:40 +0000</pubDate>
      <link>https://dev.to/rowan_feld/a-job-queue-in-postgres-two-months-later-3024</link>
      <guid>https://dev.to/rowan_feld/a-job-queue-in-postgres-two-months-later-3024</guid>
      <description>&lt;p&gt;We moved a small job queue from Redis onto Postgres two months ago, mostly to get transactions and a single backup story. Here's what actually changed.&lt;/p&gt;

&lt;p&gt;Throughput was never the problem. We run about 40 jobs a second at peak, and SELECT ... FOR UPDATE SKIP LOCKED handles that without breaking a sweat. The counter arguments about Postgres queues are usually about ten thousand a second, which we will never see.&lt;/p&gt;

&lt;p&gt;What did need thought was stuck jobs. A worker that dies mid-job leaves a locked row, and SKIP LOCKED will skip it forever. We added a locked_until column and a reaper that clears expired locks every thirty seconds. That reaper is the actual queue implementation; the select is easy.&lt;/p&gt;

&lt;p&gt;The other surprise was vacuum. A table that churns through rows fast needs its autovacuum settings tuned, or the table bloats and the queue gets slower for no obvious reason.&lt;/p&gt;

&lt;p&gt;Two months in: fewer moving parts, one backup, and a reaper job I check on Mondays. I'd make the same trade again.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>backend</category>
      <category>go</category>
    </item>
  </channel>
</rss>
