<?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: Arnab Das</title>
    <description>The latest articles on DEV Community by Arnab Das (@arnab_das_7fb7d1efcde3d09).</description>
    <link>https://dev.to/arnab_das_7fb7d1efcde3d09</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3920449%2F3ea7ce5e-aec4-4899-8d06-39eba51c5602.png</url>
      <title>DEV Community: Arnab Das</title>
      <link>https://dev.to/arnab_das_7fb7d1efcde3d09</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arnab_das_7fb7d1efcde3d09"/>
    <language>en</language>
    <item>
      <title>Building a Producer-Consumer Queue with Redis and Haskell Using Hedis</title>
      <dc:creator>Arnab Das</dc:creator>
      <pubDate>Fri, 08 May 2026 16:42:53 +0000</pubDate>
      <link>https://dev.to/arnab_das_7fb7d1efcde3d09/building-a-producer-consumer-queue-with-redis-and-haskell-using-hedis-1lg7</link>
      <guid>https://dev.to/arnab_das_7fb7d1efcde3d09/building-a-producer-consumer-queue-with-redis-and-haskell-using-hedis-1lg7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; We'll build a production-grade producer-consumer queue in Haskell using Redis as the message broker via the &lt;a href="https://hackage.haskell.org/package/hedis" rel="noopener noreferrer"&gt;Hedis&lt;/a&gt; client library. By the end, you'll have a working system that can handle high-throughput job dispatch and consumption — the same pattern I used to process &lt;strong&gt;1M+ payment refunds&lt;/strong&gt; at Juspay.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why Redis for a Queue?
&lt;/h2&gt;

&lt;p&gt;When people think "message queue," they reach for Kafka or RabbitMQ. But Redis is often the right call when you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Low latency&lt;/strong&gt; — sub-millisecond enqueue/dequeue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity&lt;/strong&gt; — no broker clusters to manage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomicity&lt;/strong&gt; — &lt;code&gt;LPUSH&lt;/code&gt;/&lt;code&gt;BRPOP&lt;/code&gt; are atomic operations, safe under concurrency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visibility&lt;/strong&gt; — you can inspect the queue state instantly with &lt;code&gt;LLEN&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At Juspay, we routed payment refunds through a Redis-backed producer-consumer system. The queue absorbed burst traffic from merchant-triggered refund events and fed a pool of consumers that processed each refund, updated sub-statuses, and called downstream banking APIs — all without a single dropped message.&lt;/p&gt;

&lt;p&gt;Let's build that.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────┐        LPUSH         ┌─────────────────┐       BRPOP        ┌──────────────┐
│   Producer   │ ──────────────────▶  │   Redis Queue   │ ─────────────────▶ │   Consumer   │
│  (Job sender)│                      │  (List: jobs)   │                    │ (Job worker) │
└──────────────┘                      └─────────────────┘                    └──────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Producer&lt;/strong&gt; pushes JSON-encoded jobs onto a Redis list using &lt;code&gt;LPUSH&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumer&lt;/strong&gt; blocks on &lt;code&gt;BRPOP&lt;/code&gt; — waking up the instant a job arrives&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple consumers&lt;/strong&gt; can run in parallel, each pulling distinct jobs atomically&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GHC + Cabal (or Stack) installed&lt;/li&gt;
&lt;li&gt;A running Redis instance (&lt;code&gt;redis-server&lt;/code&gt; or Docker: &lt;code&gt;docker run -p 6379:6379 redis&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Basic familiarity with Haskell (&lt;code&gt;do&lt;/code&gt; notation, &lt;code&gt;IO&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Project Setup
&lt;/h2&gt;

&lt;p&gt;Create a new Cabal project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;redis-queue &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;redis-queue
cabal init &lt;span class="nt"&gt;--non-interactive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add dependencies to your &lt;code&gt;redis-queue.cabal&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;build-depends:
    base        &amp;gt;= 4.14,
    hedis       &amp;gt;= 0.15,
    aeson       &amp;gt;= 2.0,
    text        &amp;gt;= 1.2,
    bytestring  &amp;gt;= 0.11,
    async       &amp;gt;= 2.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install and confirm Hedis is available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cabal build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Understanding Hedis Basics
&lt;/h2&gt;

&lt;p&gt;Hedis wraps all Redis commands in the &lt;code&gt;Redis&lt;/code&gt; monad, which you run against a &lt;code&gt;Connection&lt;/code&gt;. Here's the mental model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Open a connection pool&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;connect&lt;/span&gt; &lt;span class="n"&gt;defaultConnectInfo&lt;/span&gt;

&lt;span class="c1"&gt;-- Run Redis commands inside runRedis&lt;/span&gt;
&lt;span class="n"&gt;runRedis&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="s"&gt;"hello"&lt;/span&gt; &lt;span class="s"&gt;"world"&lt;/span&gt;
    &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s"&gt;"hello"&lt;/span&gt;   &lt;span class="c1"&gt;-- returns Right (Just "world")&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every command returns &lt;code&gt;Either Reply a&lt;/code&gt; — the &lt;code&gt;Left&lt;/code&gt; branch is a Redis protocol error, &lt;code&gt;Right&lt;/code&gt; is success. In practice you'll pattern-match or use &lt;code&gt;either&lt;/code&gt; to handle errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1 — Define the Job Type
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;src/Job.hs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="cp"&gt;{-# LANGUAGE DeriveGeneric #-}&lt;/span&gt;

&lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Job&lt;/span&gt; &lt;span class="kr"&gt;where&lt;/span&gt;

&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Data.Aeson&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;FromJSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;ToJSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Data.Text&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;GHC.Generics&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Generic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Data.ByteString.Lazy&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ByteString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- Our job payload — swap this for whatever your domain needs&lt;/span&gt;
&lt;span class="kr"&gt;data&lt;/span&gt; &lt;span class="kt"&gt;Job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Job&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;jobId&lt;/span&gt;     &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Text&lt;/span&gt;
  &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jobType&lt;/span&gt;   &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Text&lt;/span&gt;
  &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;   &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Text&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kr"&gt;deriving&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Show&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Eq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Generic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kr"&gt;instance&lt;/span&gt; &lt;span class="kt"&gt;ToJSON&lt;/span&gt;   &lt;span class="kt"&gt;Job&lt;/span&gt;
&lt;span class="kr"&gt;instance&lt;/span&gt; &lt;span class="kt"&gt;FromJSON&lt;/span&gt; &lt;span class="kt"&gt;Job&lt;/span&gt;

&lt;span class="c1"&gt;-- The Redis key we'll use as our queue&lt;/span&gt;
&lt;span class="n"&gt;queueKey&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;ByteString&lt;/span&gt;
&lt;span class="n"&gt;queueKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"jobs:queue"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping the job type generic means you can serialise anything that has a &lt;code&gt;ToJSON&lt;/code&gt; instance — refund requests, email notifications, image processing tasks, whatever fits your system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2 — The Producer
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;src/Producer.hs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Producer&lt;/span&gt; &lt;span class="kr"&gt;where&lt;/span&gt;

&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Database.Redis&lt;/span&gt;
&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Data.Aeson&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Data.ByteString.Lazy&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;toStrict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Control.Monad&lt;/span&gt;        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;forM_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Job&lt;/span&gt;

&lt;span class="c1"&gt;-- Push a single job onto the left end of the list&lt;/span&gt;
&lt;span class="n"&gt;enqueue&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Connection&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Job&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;enqueue&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
    &lt;span class="kr"&gt;let&lt;/span&gt; &lt;span class="n"&gt;encoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;toStrict&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;runRedis&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="n"&gt;lpush&lt;/span&gt; &lt;span class="n"&gt;queueKey&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;encoded&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="kr"&gt;case&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="kr"&gt;of&lt;/span&gt;
        &lt;span class="kt"&gt;Left&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;putStrLn&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="s"&gt;"Enqueue error: "&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="n"&gt;show&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="kt"&gt;Right&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;putStrLn&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="s"&gt;"Job enqueued. Queue depth: "&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="n"&gt;show&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;

&lt;span class="c1"&gt;-- Simulate a burst of jobs — e.g. end-of-day refund batch&lt;/span&gt;
&lt;span class="n"&gt;producerMain&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Connection&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;producerMain&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
    &lt;span class="kr"&gt;let&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
          &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="kt"&gt;Job&lt;/span&gt; &lt;span class="s"&gt;"txn-001"&lt;/span&gt; &lt;span class="s"&gt;"refund"&lt;/span&gt; &lt;span class="s"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;: 500,  &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;INR&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;}"&lt;/span&gt;
          &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Job&lt;/span&gt; &lt;span class="s"&gt;"txn-002"&lt;/span&gt; &lt;span class="s"&gt;"refund"&lt;/span&gt; &lt;span class="s"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;: 1200, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;INR&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;}"&lt;/span&gt;
          &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Job&lt;/span&gt; &lt;span class="s"&gt;"txn-003"&lt;/span&gt; &lt;span class="s"&gt;"notify"&lt;/span&gt; &lt;span class="s"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;user@example.com&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;}"&lt;/span&gt;
          &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Job&lt;/span&gt; &lt;span class="s"&gt;"txn-004"&lt;/span&gt; &lt;span class="s"&gt;"refund"&lt;/span&gt; &lt;span class="s"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;: 300,  &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;USD&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;}"&lt;/span&gt;
          &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Job&lt;/span&gt; &lt;span class="s"&gt;"txn-005"&lt;/span&gt; &lt;span class="s"&gt;"notify"&lt;/span&gt; &lt;span class="s"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;other@example.com&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;}"&lt;/span&gt;
          &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;putStrLn&lt;/span&gt; &lt;span class="s"&gt;"Producer starting — pushing jobs..."&lt;/span&gt;
    &lt;span class="n"&gt;forM_&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;enqueue&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;putStrLn&lt;/span&gt; &lt;span class="s"&gt;"Producer done."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key point: &lt;code&gt;lpush&lt;/code&gt; is &lt;strong&gt;atomic&lt;/strong&gt;. Even if 100 producers call it simultaneously, each job lands on the queue exactly once. Redis serialises concurrent writes internally — no locks needed on your side.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3 — The Consumer
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;src/Consumer.hs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Consumer&lt;/span&gt; &lt;span class="kr"&gt;where&lt;/span&gt;

&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Database.Redis&lt;/span&gt;
&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Data.Aeson&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Data.ByteString.Lazy&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fromStrict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Control.Monad&lt;/span&gt;        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;forever&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Job&lt;/span&gt;

&lt;span class="c1"&gt;-- Process a single job — replace this with your real business logic&lt;/span&gt;
&lt;span class="n"&gt;processJob&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Job&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;processJob&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;putStrLn&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;
    &lt;span class="s"&gt;"[Worker] Processing "&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="n"&gt;show&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobType&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;
    &lt;span class="s"&gt;" | ID: "&lt;/span&gt;              &lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="n"&gt;show&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobId&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;++&lt;/span&gt;
    &lt;span class="s"&gt;" | Payload: "&lt;/span&gt;         &lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="n"&gt;show&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;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- Block until a job is available, then process it&lt;/span&gt;
&lt;span class="n"&gt;consumeOne&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Connection&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;consumeOne&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;runRedis&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="n"&gt;brpop&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;queueKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;  &lt;span class="c1"&gt;-- 30s timeout&lt;/span&gt;
    &lt;span class="kr"&gt;case&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="kr"&gt;of&lt;/span&gt;
        &lt;span class="kt"&gt;Left&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;           &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;putStrLn&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="s"&gt;"Redis error: "&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="n"&gt;show&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="kt"&gt;Right&lt;/span&gt; &lt;span class="kt"&gt;Nothing&lt;/span&gt;      &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;putStrLn&lt;/span&gt;   &lt;span class="s"&gt;"Timeout — no jobs in 30s, polling again..."&lt;/span&gt;
        &lt;span class="kt"&gt;Right&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Just&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="kr"&gt;case&lt;/span&gt; &lt;span class="n"&gt;decode&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fromStrict&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;of&lt;/span&gt;
                &lt;span class="kt"&gt;Nothing&lt;/span&gt;  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;putStrLn&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="s"&gt;"Failed to decode job: "&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="n"&gt;show&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;
                &lt;span class="kt"&gt;Just&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;processJob&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;

&lt;span class="c1"&gt;-- Run forever, consuming jobs as they arrive&lt;/span&gt;
&lt;span class="n"&gt;consumerMain&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Connection&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;consumerMain&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;putStrLn&lt;/span&gt; &lt;span class="s"&gt;"Consumer started — waiting for jobs..."&lt;/span&gt;
    &lt;span class="n"&gt;forever&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;consumeOne&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;brpop&lt;/code&gt; is the magic here. It &lt;strong&gt;blocks the connection&lt;/strong&gt; until an item is available on any of the listed keys, then atomically pops and returns it. The &lt;code&gt;30&lt;/code&gt; is a timeout in seconds — after which it returns &lt;code&gt;Right Nothing&lt;/code&gt; so you can loop cleanly rather than hanging forever.&lt;/p&gt;

&lt;p&gt;This is fundamentally different from polling (&lt;code&gt;RPOP&lt;/code&gt; in a loop with &lt;code&gt;threadDelay&lt;/code&gt;) — blocking means zero CPU burn while the queue is empty.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4 — Wire It Together
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;app/Main.hs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Main&lt;/span&gt; &lt;span class="kr"&gt;where&lt;/span&gt;

&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Database.Redis&lt;/span&gt;
&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Control.Concurrent.Async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;concurrently_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Producer&lt;/span&gt;
&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Consumer&lt;/span&gt;

&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;-- Connect to local Redis; swap defaultConnectInfo for your host/port/auth&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;connect&lt;/span&gt; &lt;span class="n"&gt;defaultConnectInfo&lt;/span&gt;

    &lt;span class="c1"&gt;-- Run producer and consumer concurrently&lt;/span&gt;
    &lt;span class="c1"&gt;-- In production you'd run these as separate processes/services&lt;/span&gt;
    &lt;span class="n"&gt;concurrently_&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;producerMain&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;consumerMain&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;concurrently_&lt;/code&gt; from the &lt;code&gt;async&lt;/code&gt; package runs both actions in parallel on separate OS threads, waiting for both to finish. In a real deployment you'd run the producer and consumer as separate services — this just wires them together for a clean demo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Terminal 1 — start Redis&lt;/span&gt;
redis-server

&lt;span class="c"&gt;# Terminal 2 — run the app&lt;/span&gt;
cabal run redis-queue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Producer starting — pushing jobs...
Job enqueued. Queue depth: 1
Job enqueued. Queue depth: 2
Job enqueued. Queue depth: 3
Job enqueued. Queue depth: 4
Job enqueued. Queue depth: 5
Producer done.
Consumer started — waiting for jobs...
[Worker] Processing "refund" | ID: "txn-001" | Payload: "{"amount": 500, "currency": "INR"}"
[Worker] Processing "refund" | ID: "txn-002" | Payload: "{"amount": 1200, "currency": "INR"}"
[Worker] Processing "notify" | ID: "txn-003" | Payload: "{"email": "user@example.com"}"
[Worker] Processing "refund" | ID: "txn-004" | Payload: "{"amount": 300, "currency": "USD"}"
[Worker] Processing "notify" | ID: "txn-005" | Payload: "{"email": "other@example.com"}"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 5 — Scaling to Multiple Consumers
&lt;/h2&gt;

&lt;p&gt;Want parallel workers? Spawn multiple consumers against the same queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Control.Concurrent.Async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;replicateConcurrently_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;connect&lt;/span&gt; &lt;span class="n"&gt;defaultConnectInfo&lt;/span&gt;
    &lt;span class="c1"&gt;-- Run 4 parallel consumer workers&lt;/span&gt;
    &lt;span class="n"&gt;concurrently_&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;producerMain&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;replicateConcurrently_&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;consumerMain&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;BRPOP&lt;/code&gt; is atomic, each job is delivered to &lt;strong&gt;exactly one&lt;/strong&gt; consumer — no double-processing. Redis handles the fan-out natively.&lt;/p&gt;

&lt;p&gt;You can verify this live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In a Redis CLI while the app runs:&lt;/span&gt;
redis-cli LLEN &lt;span class="nb"&gt;jobs&lt;/span&gt;:queue   &lt;span class="c"&gt;# current queue depth&lt;/span&gt;
redis-cli MONITOR           &lt;span class="c"&gt;# watch every command in real time&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 6 — Dead Letter Handling (Production Hardening)
&lt;/h2&gt;

&lt;p&gt;In production, jobs can fail. You don't want failed jobs silently disappearing. Add a dead-letter queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="n"&gt;deadLetterKey&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;ByteString&lt;/span&gt;
&lt;span class="n"&gt;deadLetterKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"jobs:dead"&lt;/span&gt;

&lt;span class="c1"&gt;-- Consume with failure handling&lt;/span&gt;
&lt;span class="n"&gt;consumeSafe&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Connection&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;consumeSafe&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;runRedis&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="n"&gt;brpop&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;queueKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
    &lt;span class="kr"&gt;case&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="kr"&gt;of&lt;/span&gt;
        &lt;span class="kt"&gt;Right&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Just&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="kr"&gt;case&lt;/span&gt; &lt;span class="n"&gt;decode&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fromStrict&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;of&lt;/span&gt;
                &lt;span class="kt"&gt;Nothing&lt;/span&gt;  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
                    &lt;span class="c1"&gt;-- Malformed payload — send to dead letter queue&lt;/span&gt;
                    &lt;span class="kr"&gt;_&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;runRedis&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="n"&gt;lpush&lt;/span&gt; &lt;span class="n"&gt;deadLetterKey&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                    &lt;span class="n"&gt;putStrLn&lt;/span&gt; &lt;span class="s"&gt;"Malformed job moved to dead letter queue"&lt;/span&gt;
                &lt;span class="kt"&gt;Just&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
                    &lt;span class="c1"&gt;-- Wrap in exception handler for business logic failures&lt;/span&gt;
                    &lt;span class="n"&gt;processJob&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="p"&gt;`&lt;/span&gt;&lt;span class="n"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;`&lt;/span&gt; &lt;span class="nf"&gt;\&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;SomeException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
                        &lt;span class="kr"&gt;_&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;runRedis&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="n"&gt;lpush&lt;/span&gt; &lt;span class="n"&gt;deadLetterKey&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                        &lt;span class="n"&gt;putStrLn&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="s"&gt;"Job failed, dead-lettered: "&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="n"&gt;show&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
        &lt;span class="kr"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pure&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now failed jobs accumulate in &lt;code&gt;jobs:dead&lt;/code&gt; where you can inspect, replay, or alert on them — no silent data loss.&lt;/p&gt;




&lt;h2&gt;
  
  
  Connecting to a Real Redis Host
&lt;/h2&gt;

&lt;p&gt;For production (Redis Cloud, AWS ElastiCache, etc.):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;Database.Redis&lt;/span&gt;

&lt;span class="n"&gt;productionConnInfo&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;ConnectInfo&lt;/span&gt;
&lt;span class="n"&gt;productionConnInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;defaultConnectInfo&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;connectHost&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"your-redis-host.example.com"&lt;/span&gt;
    &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connectPort&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;PortNumber&lt;/span&gt; &lt;span class="mi"&gt;6379&lt;/span&gt;
    &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connectAuth&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Just&lt;/span&gt; &lt;span class="s"&gt;"your-auth-password"&lt;/span&gt;
    &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connectDatabase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connectMaxConnections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;   &lt;span class="c1"&gt;-- connection pool size&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="nb"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;connect&lt;/span&gt; &lt;span class="n"&gt;productionConnInfo&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For TLS (Redis Cloud, Upstash, etc.), use &lt;code&gt;checkedConnect&lt;/code&gt; with &lt;code&gt;connectTLSParams&lt;/code&gt; set.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Built vs. What Juspay Ran
&lt;/h2&gt;

&lt;p&gt;The pattern here is the same core design behind Juspay's refund processing pipeline — with a few additions at scale:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;This Tutorial&lt;/th&gt;
&lt;th&gt;Production at Juspay&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;In-memory job type&lt;/td&gt;
&lt;td&gt;Protobuf-encoded payloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single queue key&lt;/td&gt;
&lt;td&gt;Separate queues per refund type/priority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;brpop&lt;/code&gt; timeout loop&lt;/td&gt;
&lt;td&gt;Supervised consumer pools with health checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;putStrLn&lt;/code&gt; processing&lt;/td&gt;
&lt;td&gt;Downstream bank API calls + DB writes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic dead-letter&lt;/td&gt;
&lt;td&gt;Dead-letter + retry with exponential backoff&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Redis primitives (&lt;code&gt;LPUSH&lt;/code&gt;, &lt;code&gt;BRPOP&lt;/code&gt;, atomic pops) are identical. Scaling up is mostly operational — more consumer replicas, queue-per-priority, monitoring via &lt;code&gt;LLEN&lt;/code&gt; metrics fed into dashboards.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LPUSH&lt;/code&gt; + &lt;code&gt;BRPOP&lt;/code&gt;&lt;/strong&gt; is Redis's native producer-consumer primitive — atomic, fast, and simple&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hedis&lt;/strong&gt; gives you a type-safe, monadic interface to Redis in Haskell with connection pooling built in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blocking pop (&lt;code&gt;BRPOP&lt;/code&gt;)&lt;/strong&gt; beats polling — zero CPU overhead while the queue is idle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dead-letter queues&lt;/strong&gt; are non-negotiable in production — never let failed jobs disappear silently&lt;/li&gt;
&lt;li&gt;This pattern scales horizontally: add consumers, add producers, the queue fans out automatically&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Full Source Code
&lt;/h2&gt;

&lt;p&gt;The complete working project is on GitHub: &lt;a href="https://github.com/arnabdas1999" rel="noopener noreferrer"&gt;https://github.com/arnabdas1999/redis-hedis-queue&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Priority queues&lt;/strong&gt; — use multiple Redis lists (&lt;code&gt;jobs:high&lt;/code&gt;, &lt;code&gt;jobs:low&lt;/code&gt;) and pass both keys to &lt;code&gt;BRPOP&lt;/code&gt;; Redis pops from the first non-empty list&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delayed jobs&lt;/strong&gt; — use a Redis Sorted Set with the scheduled timestamp as the score; a scheduler process moves ready jobs to the main queue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exactly-once delivery&lt;/strong&gt; — combine &lt;code&gt;BRPOPLPUSH&lt;/code&gt; with a processing list and a visibility timeout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drop questions in the comments — happy to dig into any of these.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Arnab Das is an MS student at NYU Tandon and a software engineer who worked on payment infrastructure at Juspay, processing 200M+ daily transactions. Find him on &lt;a href="https://linkedin.com/in/arnab-das-772b1220b" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; and &lt;a href="https://github.com/arnabdas1999" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>redis</category>
      <category>haskell</category>
      <category>tutorial</category>
      <category>distributedsystems</category>
    </item>
  </channel>
</rss>
