<?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: Serhat</title>
    <description>The latest articles on DEV Community by Serhat (@zulwatha).</description>
    <link>https://dev.to/zulwatha</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%2F4058962%2Faa3390b1-6de1-4bca-9ff4-fe754fc69303.jpg</url>
      <title>DEV Community: Serhat</title>
      <link>https://dev.to/zulwatha</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zulwatha"/>
    <language>en</language>
    <item>
      <title>I got tired of running Redis for one background job, so I built a SQLite job queue</title>
      <dc:creator>Serhat</dc:creator>
      <pubDate>Sun, 02 Aug 2026 10:47:22 +0000</pubDate>
      <link>https://dev.to/zulwatha/i-got-tired-of-running-redis-for-one-background-job-so-i-built-a-sqlite-job-queue-4flk</link>
      <guid>https://dev.to/zulwatha/i-got-tired-of-running-redis-for-one-background-job-so-i-built-a-sqlite-job-queue-4flk</guid>
      <description>&lt;p&gt;Every side project I've shipped in the last few years has hit the same wall at roughly the same point. The app works, it runs on a single cheap VPS, and then I need to send an email &lt;em&gt;after&lt;/em&gt; the request returns. Or resize an image. Or run something every night at 3am.&lt;/p&gt;

&lt;p&gt;The standard answer in Node land is BullMQ, which means Redis. And Redis is great. But now my "one small app on one small server" has a second service to install, monitor, secure, and remember exists when the box reboots. All so I can send an email five seconds later.&lt;/p&gt;

&lt;p&gt;The third time I caught myself provisioning Redis for a queue that would see maybe two hundred jobs a day, I stopped and wrote &lt;a href="https://github.com/Zulwatha/vardiya" rel="noopener noreferrer"&gt;vardiya&lt;/a&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;If your whole app lives on one machine, you already have a durable, transactional store sitting right there: the disk. SQLite in WAL mode handles concurrent readers and a single writer perfectly well, and a job queue is mostly just "one writer claims rows atomically."&lt;/p&gt;

&lt;p&gt;So vardiya keeps the entire queue in a SQLite file. One runtime dependency (&lt;code&gt;better-sqlite3&lt;/code&gt;). Cron parsing, backoff math, and id generation are all in-house, so there's no dependency tree to audit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Vardiya&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vardiya&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Vardiya&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;databasePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./jobs.sqlite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a@b.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createWorker&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole setup. No broker URL, no connection retries, no second process. When your app dies, the file is still there, and so are your jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually covers
&lt;/h2&gt;

&lt;p&gt;I didn't want a toy that falls over the moment you use it seriously, so the boring-but-necessary parts are in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retries with fixed or exponential backoff, optional jitter, and a dead-letter state when &lt;code&gt;maxAttempts&lt;/code&gt; runs out&lt;/li&gt;
&lt;li&gt;Delayed jobs, priorities, and custom &lt;code&gt;jobId&lt;/code&gt; for dedup when your producer might fire twice&lt;/li&gt;
&lt;li&gt;Repeatable jobs via 5-field cron (plus &lt;code&gt;@hourly&lt;/code&gt;, &lt;code&gt;@daily&lt;/code&gt;, &lt;code&gt;@weekly&lt;/code&gt;, &lt;code&gt;@monthly&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Heartbeats and stalled-job reclaim, so a crashed worker doesn't strand jobs in "active" forever&lt;/li&gt;
&lt;li&gt;Atomic claim via a single &lt;code&gt;UPDATE ... RETURNING&lt;/code&gt;, so multiple worker processes on the same file won't grab the same job&lt;/li&gt;
&lt;li&gt;Typed events for everything (&lt;code&gt;job:completed&lt;/code&gt;, &lt;code&gt;job:failed&lt;/code&gt;, &lt;code&gt;job:dead&lt;/code&gt;, ...)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On my machine the bench does around 13k enqueues/sec and roughly 5k processed jobs/sec end-to-end. Your disk will disagree with my disk, so run &lt;code&gt;npm run bench&lt;/code&gt; yourself before quoting numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;vardiya is at-least-once, and I say so in the README instead of burying it. A job &lt;em&gt;can&lt;/em&gt; run twice if a worker dies after doing the work but before recording completion. Every crash-safe queue has this property; the ones that claim exactly-once are just moving the problem into your side effects. Write idempotent handlers, use &lt;code&gt;jobId&lt;/code&gt; as a dedup token, and you're fine.&lt;/p&gt;

&lt;p&gt;Also: SQLite is a single-writer database. Multiple processes on one host sharing a local file works. A fleet of app servers fighting over a network mount will not, and I'm not going to pretend otherwise. If you need many machines pulling from one logical queue, BullMQ or pg-boss are the right tools and I'd use them myself. Same if you want dashboards, rate-limit groups, or sandboxed processors. Those are product features other projects spent years on, and vardiya is deliberately just the queue core.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "vardiya"?
&lt;/h2&gt;

&lt;p&gt;It's Turkish for "work shift." Workers clocking in, picking up jobs, clocking out. It felt right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;vardiya
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requires Node &amp;gt;=22. The repo has examples, the full API reference, and a comparison table against BullMQ / pg-boss / bee-queue if you want the details: &lt;a href="https://github.com/Zulwatha/vardiya" rel="noopener noreferrer"&gt;github.com/Zulwatha/vardiya&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's MIT licensed and young. Issues and PRs are very welcome, especially war stories from anyone who has pushed SQLite queues harder than I have. And if the "one VPS, no Redis" shape matches your app, I'd genuinely like to hear whether it holds up for you.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>node</category>
      <category>sqlite</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
