<?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: Density Tech</title>
    <description>The latest articles on DEV Community by Density Tech (@density_tech).</description>
    <link>https://dev.to/density_tech</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%2F3729517%2F405000b6-b607-4f8c-b83c-c607fdf7130e.png</url>
      <title>DEV Community: Density Tech</title>
      <link>https://dev.to/density_tech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/density_tech"/>
    <language>en</language>
    <item>
      <title>We Tried to Build Analytics Without a Database. It Sort of Worked.</title>
      <dc:creator>Density Tech</dc:creator>
      <pubDate>Thu, 05 Feb 2026 17:40:13 +0000</pubDate>
      <link>https://dev.to/density_tech/we-tried-to-build-analytics-without-a-database-it-sort-of-worked-396</link>
      <guid>https://dev.to/density_tech/we-tried-to-build-analytics-without-a-database-it-sort-of-worked-396</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiu3q2s77jtavneu8nxme.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiu3q2s77jtavneu8nxme.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The client needed product analytics.&lt;/p&gt;

&lt;p&gt;The problem wasn’t scale.&lt;br&gt;
The problem was money.&lt;/p&gt;

&lt;p&gt;Snowflake’s minimum was roughly $600 per month. The total infrastructure budget was closer to $200. Given that constraint, debating which data warehouse to use felt like the wrong conversation to have.&lt;/p&gt;

&lt;p&gt;So instead, we asked a different question:&lt;/p&gt;

&lt;p&gt;What if we didn’t use a database at all?&lt;/p&gt;

&lt;h2&gt;
  
  
  Dumping Everything into Object Storage
&lt;/h2&gt;

&lt;p&gt;The first decision was straightforward: use object storage.&lt;/p&gt;

&lt;p&gt;For this engagement, we chose MinIO. Events were ingested, written out as Parquet files, and stored durably. No long-running services. No query engine sitting idle. Just storage.&lt;/p&gt;

&lt;p&gt;That immediately raised the obvious concern:&lt;br&gt;
how do you query this without turning it into an unmaintainable pile of files?&lt;/p&gt;

&lt;p&gt;That’s where DuckDB entered the picture.&lt;/p&gt;

&lt;p&gt;DuckDB is an in-process SQL engine. No server, no cluster, no operational setup. Install it, point it at Parquet files, and start writing SQL.&lt;/p&gt;

&lt;p&gt;Initially, it felt too simple to be serious.&lt;/p&gt;

&lt;p&gt;We tried it anyway.&lt;/p&gt;

&lt;p&gt;Within a day, we had funnel queries, retention calculations, and basic aggregations running directly against Parquet files in S3-compatible storage. No ingestion jobs. No warehouse loaders. No retry logic.&lt;/p&gt;

&lt;p&gt;It worked far better than expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Iceberg Detour That Changed Everything
&lt;/h2&gt;

&lt;p&gt;About two weeks into the project, Apache Iceberg came up during a casual discussion.&lt;/p&gt;

&lt;p&gt;At that point, we weren’t actively looking to change anything. The system was working. But Iceberg promised things that were starting to matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ACID semantics&lt;/li&gt;
&lt;li&gt;Schema evolution&lt;/li&gt;
&lt;li&gt;Snapshot isolation&lt;/li&gt;
&lt;li&gt;Table-level operations on object storage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We treated it as an experiment.&lt;/p&gt;

&lt;p&gt;That experiment quietly became the foundation.&lt;/p&gt;

&lt;p&gt;Once we moved from loosely managed Parquet files to Iceberg tables, several problems disappeared immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schema changes became manageable&lt;/li&gt;
&lt;li&gt;Bad data was no longer permanent&lt;/li&gt;
&lt;li&gt;Table state became explicit and queryable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DuckDB’s Iceberg extension made the integration trivial. One &lt;code&gt;ATTACH&lt;/code&gt;, and the tables behaved like standard relational tables.&lt;/p&gt;

&lt;p&gt;The value became obvious the first time a customer sent several hours of malformed events. Previously, fixing that would have meant manually tracking files and rewriting data. With Iceberg, it was a single &lt;code&gt;DELETE&lt;/code&gt; statement.&lt;/p&gt;

&lt;p&gt;That alone justified the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local Development Without Friction
&lt;/h2&gt;

&lt;p&gt;One outcome that surprised me was how much this improved local development.&lt;/p&gt;

&lt;p&gt;DuckDB’s UI mode (&lt;code&gt;duckdb -ui&lt;/code&gt;) provides a browser-based SQL editor running entirely on a developer’s machine. No credentials to manage. No shared environments. No waiting for services to start.&lt;/p&gt;

&lt;p&gt;Even developers who typically avoid SQL were able to explore real analytics data locally.&lt;/p&gt;

&lt;p&gt;That level of accessibility is rare in analytics systems—and for an MVP, it mattered more than raw throughput.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Limitations (Because There Are Always Limitations)
&lt;/h2&gt;

&lt;p&gt;This setup isn’t a silver bullet.&lt;/p&gt;

&lt;p&gt;DuckDB is not designed for high concurrency. Once concurrent access increased, caching became necessary. Event ingestion is buffered and batched. Writes are controlled and intentionally infrequent.&lt;/p&gt;

&lt;p&gt;This is not a multi-tenant analytics platform.&lt;/p&gt;

&lt;p&gt;And it was never meant to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Actually Delivered
&lt;/h2&gt;

&lt;p&gt;In roughly three weeks, the client had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A functional product analytics backend&lt;/li&gt;
&lt;li&gt;Iceberg-managed tables on object storage&lt;/li&gt;
&lt;li&gt;DuckDB-powered analytical queries and derivations&lt;/li&gt;
&lt;li&gt;Infrastructure costs under $50 per month&lt;/li&gt;
&lt;li&gt;A system solid enough to demo to customers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly, they avoided committing early to an expensive or complex architecture before the product justified it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Takeaway
&lt;/h2&gt;

&lt;p&gt;From my perspective, if you’re pre–product-market fit, a data warehouse is often the wrong first move.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You don’t need infinite scale.&lt;/li&gt;
&lt;li&gt;You don’t need query queues.&lt;/li&gt;
&lt;li&gt;You don’t need vendor contracts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You need speed, flexibility, and the ability to change direction without regret.&lt;/p&gt;

&lt;p&gt;DuckDB plus object storage—and Iceberg when structure starts to matter—isn’t just cheaper. It’s better suited for experimentation.&lt;/p&gt;

&lt;p&gt;We’ll move to something heavier when it’s required.&lt;/p&gt;

&lt;p&gt;For now, this works. And as an engineer, it’s been a genuinely enjoyable system to build.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>opensource</category>
      <category>database</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What Actually Happens When You Run brew install</title>
      <dc:creator>Density Tech</dc:creator>
      <pubDate>Wed, 28 Jan 2026 18:21:18 +0000</pubDate>
      <link>https://dev.to/density_tech/what-actually-happens-when-you-run-brew-install-3j3n</link>
      <guid>https://dev.to/density_tech/what-actually-happens-when-you-run-brew-install-3j3n</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fou3ix17rq929mjjafkbf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fou3ix17rq929mjjafkbf.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
The &lt;code&gt;brew install&lt;/code&gt; command is used to install software on your computer.&lt;/p&gt;

&lt;p&gt;This command is pretty useful because it makes it easy to get the software you need.&lt;/p&gt;

&lt;p&gt;When you run &lt;code&gt;brew install&lt;/code&gt; it looks for the software you want. Then it downloads it.&lt;/p&gt;

&lt;p&gt;Then it installs the software on your computer.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;brew install&lt;/code&gt; command also takes care of any software that is required for the software you are installing to work properly.&lt;/p&gt;

&lt;p&gt;For example if the software you are installing needs software to run &lt;code&gt;brew install&lt;/code&gt; will download and install that software too.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;brew&lt;/code&gt; is a handy tool to have on your computer.&lt;/p&gt;

&lt;p&gt;You can use brew install to install all sorts of software like text editors and programming tools.&lt;/p&gt;

&lt;p&gt;So the time you need to install some software you can use brew install to do it.&lt;/p&gt;

&lt;p&gt;The brew install command is a help when you are working with software on your computer.&lt;/p&gt;

&lt;p&gt;Most developers use Homebrew every day. Very few people actually think about what Homebrew is really doing. Homebrew looks like a package manager to them. Homebrew feels like &lt;code&gt;apt&lt;/code&gt; or &lt;code&gt;yum&lt;/code&gt; when they use it.. Inside Homebrew it does things in a very different way. Homebrew behaves a lot, like Git when it is working. Homebrew also does things like a build system. It keeps the filesystem in order.&lt;/p&gt;

&lt;p&gt;When you finally get it a lot of things that your system does will start to make sense. You will be able to figure out a lot of system problems that seemed weird before like why your system stopped working.&lt;/p&gt;

&lt;p&gt;Let us open the hood.&lt;/p&gt;

&lt;p&gt;Homebrew is not something that installs things for you. Homebrew is actually a place where you can find things to build. Think of Homebrew as a list of things you can build on your computer. Homebrew is, like a registry that keeps track of all the things you can build.&lt;/p&gt;

&lt;p&gt;When you run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew install docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not mean that you should download &lt;strong&gt;Docker&lt;/strong&gt; and then put it somewhere on your computer. Docker is not something that you can just download and place anywhere.&lt;/p&gt;

&lt;p&gt;What this thing really means is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Look up a build recipe for Docker, find a verified binary (or source), install it into a versioned directory, and expose it through symlinks.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Homebrew&lt;/strong&gt; is actually like a place where you can find and manage builds from the source rather than what you would normally think of as a package manager. Homebrew is really good, at keeping track of all the builds you have kind of like a big library of stuff you have put together yourself.&lt;/p&gt;

&lt;p&gt;Every package is defined by a formula that is written in &lt;strong&gt;Ruby&lt;/strong&gt;. This formula is stored in a Git repository called &lt;a href="https://github.com/Homebrew/homebrew-core" rel="noopener noreferrer"&gt;homebrew-core&lt;/a&gt;. The homebrew-core repository contains things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Where to download the binary&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What checksum should the file have? The correct checksum, for the file is important to know. We need to find out what checksum the file should have.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When we are talking about something that needs to be built or unpacked the first thing that comes to my mind is that we should have a set of instructions.&lt;/p&gt;

&lt;p&gt;The instructions for building or unpacking the thing should be easy to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cellar: Homebrew’s real filesystem
&lt;/h2&gt;

&lt;p&gt;Everything Homebrew installs goes into one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opt/homebrew/Cellar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(on Apple Silicon Macs)&lt;/p&gt;

&lt;p&gt;Inside that directory, every package gets its own folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opt/homebrew/Cellar/docker/29.1.5/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That folder contains the real Docker binary and everything it depends on. Homebrew never mutates it. That directory is immutable. If you install a new version, you get a new directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opt/homebrew/Cellar/docker/29.1.6/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing overwrites anything. No files are replaced in place. This is a huge design choice — and it’s why Homebrew is so safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  How commands appear in your PATH
&lt;/h2&gt;

&lt;p&gt;You don’t run binaries from the Cellar directly. Instead, Homebrew creates symlinks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opt/homebrew/bin/docker → ../Cellar/docker/29.1.5/bin/docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your shell sees /opt/homebrew/bin in PATH, so when you type docker, it follows the symlink to the correct version.&lt;/p&gt;

&lt;p&gt;When you upgrade Docker, Homebrew just moves the symlink to point to the new directory. The old version is still there, untouched.&lt;/p&gt;

&lt;p&gt;This is why Homebrew supports instant rollbacks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew switch docker 29.1.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All it does is change symlinks.&lt;/p&gt;

&lt;p&gt;No files are copied. Nothing is rebuilt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Homebrew almost never corrupts your system
&lt;/h2&gt;

&lt;p&gt;Because Homebrew never installs into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/bin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/lib
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;system frameworks&lt;/p&gt;

&lt;p&gt;Everything lives under &lt;code&gt;/opt/homebrew&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That’s not an accident — it’s a safety boundary. If something breaks, you can literally delete &lt;code&gt;/opt/homebrew&lt;/code&gt; and your OS is still intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottles vs source builds
&lt;/h2&gt;

&lt;p&gt;When Homebrew can, it downloads a bottle — a precompiled binary built by the Homebrew maintainers. That’s why installs are fast.&lt;/p&gt;

&lt;p&gt;If no bottle exists for your OS + CPU combo, Homebrew falls back to compiling from source using the instructions in the formula. Same recipe, different execution path.&lt;/p&gt;

&lt;p&gt;Either way, the result still lands in the Cellar.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Homebrew&lt;/strong&gt; looks simple because Homebrew is actually hiding a deliberate architecture. The people who made Homebrew did a lot of thinking about how Homebrew should work. They wanted Homebrew to be easy to use so they made sure that the complicated parts of Homebrew are not visible, to the user. This means that Homebrew has a lot of things going on behind the scenes that you do not see when you use Homebrew.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>beginners</category>
      <category>docker</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Do You Really Need Kafka? A Practical Alternative with Postgres</title>
      <dc:creator>Density Tech</dc:creator>
      <pubDate>Sun, 25 Jan 2026 09:18:31 +0000</pubDate>
      <link>https://dev.to/density_tech/do-you-really-need-kafka-a-practical-alternative-with-postgres-2de8</link>
      <guid>https://dev.to/density_tech/do-you-really-need-kafka-a-practical-alternative-with-postgres-2de8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fovlnkrzhmz952fkcjv7e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fovlnkrzhmz952fkcjv7e.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Kafka: The Right Tool, Used Too Often
&lt;/h2&gt;

&lt;p&gt;Apache Kafka has become the default answer to almost any asynchronous or event-driven problem. It is powerful, proven at scale, and excellent at handling large volumes of data with strong guarantees. If you are building a real-time data platform, a streaming system, or anything that needs to fan out events to many consumers, Kafka is often the right tool.&lt;/p&gt;

&lt;p&gt;But Kafka also comes with real cost. Running it in production means dealing with brokers, partitions, consumer groups, rebalancing, retention policies, and monitoring. Even with managed services, you are still paying in infrastructure and in engineering time.&lt;/p&gt;

&lt;p&gt;In practice, many teams end up using Kafka not because they need a streaming platform, but because they just need a reliable queue.&lt;/p&gt;

&lt;p&gt;And in those cases, Kafka is usually correct — but often overkill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Cost of “Just Use Kafka”
&lt;/h2&gt;

&lt;p&gt;For early-stage systems, internal tools, or moderate workloads, Kafka tends to introduce more complexity than actual value.&lt;/p&gt;

&lt;p&gt;You run a distributed system even when your problem is not distributed.&lt;br&gt;
You operate a streaming platform even when your use case is just background jobs.&lt;br&gt;
You manage offsets and consumer groups when a simple retry would be enough.&lt;br&gt;
You debug through dashboards and logs instead of just looking at the data.&lt;/p&gt;

&lt;p&gt;The result is a system that works well, but feels heavy. Heavy to run, heavy to reason about, and heavy to change.&lt;/p&gt;

&lt;p&gt;This is exactly where Postgres-based queues start to look very attractive.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Postgres Can Win in Economics and Debuggability
&lt;/h2&gt;

&lt;p&gt;Postgres is already there in almost every backend. It is monitored, backed up, and familiar. Using it as a lightweight message queue adds no new service, no new cluster, and no new operational surface.&lt;/p&gt;

&lt;p&gt;From a cost perspective, it is hard to beat:&lt;br&gt;
there are no brokers to run, no separate infrastructure, and no extra managed services to pay for.&lt;/p&gt;

&lt;p&gt;From a debugging perspective, it is even better:&lt;br&gt;
every message is just a row,&lt;br&gt;
every failure is visible,&lt;br&gt;
retries are tracked,&lt;br&gt;
and stuck messages can be inspected or fixed with plain SQL.&lt;/p&gt;

&lt;p&gt;Instead of debugging a distributed system, you debug data.&lt;br&gt;
And for most engineers, that is a much simpler and more productive mental model.&lt;/p&gt;
&lt;h2&gt;
  
  
  When Postgres Is Actually the Right Choice
&lt;/h2&gt;

&lt;p&gt;Postgres works well as a message queue when async processing is just a part of your system, not the main thing your system exists to do. In these cases, you usually care more about simplicity and reliability than extreme scale or global distribution.&lt;/p&gt;

&lt;p&gt;pgmq fits nicely for things like background jobs, webhook handling, retry systems, internal workflows, and small ETL pipelines. These setups usually have a few producers, a few consumers, and traffic that is steady but not massive. What they really need is visibility and control, not a full-blown streaming platform.&lt;/p&gt;

&lt;p&gt;This is where Postgres shines. You can wrap business logic and queue operations in the same transaction. You don’t need to run any extra infrastructure. And you can see exactly what’s happening just by querying tables. If something breaks, you can inspect the message, fix it, and retry it directly.&lt;/p&gt;

&lt;p&gt;pgmq is not meant for high-throughput streaming, analytics pipelines, or cross-region event systems. Once the queue becomes the core of your architecture, and not just a helper, you are in Kafka territory.&lt;/p&gt;

&lt;p&gt;The simple rule is: use Postgres when the queue supports your system. Use Kafka when the queue is your system.&lt;/p&gt;
&lt;h2&gt;
  
  
  pgmq: How It Works
&lt;/h2&gt;

&lt;p&gt;At a high level, &lt;strong&gt;pgmq&lt;/strong&gt; is not doing anything magical. It is just using Postgres tables, locks, and timestamps to behave like a message queue. There is no separate broker, no background service, and no hidden state. Everything lives inside the database.&lt;/p&gt;

&lt;p&gt;When you create a queue in pgmq, it creates two main tables for you:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pgmq.q_events&lt;/code&gt; – the live queue&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pgmq.a_events&lt;/code&gt; – the history of processed messages&lt;/p&gt;

&lt;p&gt;The live table is where all active messages sit. Each row is one message. The important columns are:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;msg_id&lt;/code&gt; – unique ID for the message&lt;/p&gt;

&lt;p&gt;&lt;code&gt;enqueued_at&lt;/code&gt; – when the message was produced&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vt&lt;/code&gt; – when the message can be read again&lt;/p&gt;

&lt;p&gt;&lt;code&gt;read_ct&lt;/code&gt; – how many times it has been delivered&lt;/p&gt;

&lt;p&gt;&lt;code&gt;message&lt;/code&gt; – your actual JSON payload&lt;/p&gt;

&lt;p&gt;&lt;code&gt;headers&lt;/code&gt; - your actual headers&lt;/p&gt;

&lt;p&gt;This single table gives you most queue features in one place:&lt;/p&gt;

&lt;p&gt;Durability → rows stored in Postgres&lt;/p&gt;

&lt;p&gt;Visibility timeout → &lt;code&gt;vt&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Retry count → &lt;code&gt;read_ct&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Ordering → &lt;code&gt;ORDER BY msg_id&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Backlog → &lt;code&gt;SELECT count(*)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;When a consumer reads messages, pgmq simply locks rows using &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; and moves &lt;code&gt;vt&lt;/code&gt; into the future. If the consumer crashes, the lock is released and the message becomes visible again. That is your retry mechanism.&lt;/p&gt;

&lt;p&gt;When the consumer finishes, calling &lt;code&gt;pgmq.delete()&lt;/code&gt; removes the row from &lt;code&gt;q_events&lt;/code&gt; and moves it into &lt;code&gt;a_events&lt;/code&gt;. That archive table is extremely useful in practice — it gives you a full audit trail of what was processed, when, and how many times.&lt;/p&gt;

&lt;p&gt;There is also a small &lt;code&gt;pgmq.meta&lt;/code&gt; table which stores queue-level configuration like visibility timeouts and creation metadata. Think of it as the control plane.&lt;/p&gt;

&lt;p&gt;The key thing to understand is this: pgmq is just SQL implementing queue semantics. If you can read the tables, you can understand the system. There is no black box. What you see in the database is exactly what the queue is doing.&lt;/p&gt;

&lt;p&gt;And that is precisely why pgmq feels so easy to debug compared to traditional brokers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In the next section, we’ll set up a minimal pgmq environment using Docker and Kubernetes, and walk through a working producer–consumer example.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Setting Up pgmq Locally (A Minimal Working Example)
&lt;/h2&gt;

&lt;p&gt;We’ll start by running Postgres with pgmq using a single Kubernetes deployment.&lt;/p&gt;

&lt;p&gt;Prerequisties : Docker, Minikube&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# postgres.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: ghcr.io/pgmq/pg18-pgmq:v1.7.0
          imagePullPolicy: IfNotPresent
          env:
            - name: POSTGRES_USER
              value: xxx
            - name: POSTGRES_PASSWORD
              value: xxx
            - name: POSTGRES_DB
              value: queue_db
          ports:
            - containerPort: 5432
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  type: NodePort
  selector:
    app: postgres
  ports:
    - port: 5432
      nodePort: 30007


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl apply -f postgres.yaml
kubectl get pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl port-forward svc/postgres 5432:5432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In new Terminal&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;psql -h localhost -U xxx -d queue_db

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To create a queue&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE EXTENSION pgmq;
SELECT pgmq.create('events');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Producer Setup (Python)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# producer.py

import psycopg2
import json
import time

conn = psycopg2.connect(
    host="postgres",
    port=5432,
    user="xxx",
    password="xxx",
    dbname="queue_db"
)

cur = conn.cursor()
i = 0

while True:
    payload = {"id": i, "type": "order_created"}
    cur.execute("SELECT pgmq.send('events', %s)", [json.dumps(payload)])
    conn.commit()
    print("Produced:", payload)
    i += 1
    time.sleep(1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Dockerfile

FROM python:3.11-slim
WORKDIR /app
RUN pip install psycopg2-binary
COPY producer.py .
CMD ["python", "producer.py"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# producer.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pg-producer
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pg-producer
  template:
    metadata:
      labels:
        app: pg-producer
    spec:
      containers:
        - name: producer
          image: pg-producer
          imagePullPolicy: IfNotPresent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
docker build -t pg-producer .
kubectl apply -f producer.yaml

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Consumer Setup (Python)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# consumer.py

import psycopg2
import time

conn = psycopg2.connect(
    host="postgres",
    port=5432,
    user="user",
    password="pass",
    dbname="queue_db"
)

cur = conn.cursor()

while True:
    cur.execute("SELECT * FROM pgmq.read('events', 1, 5)")
    rows = cur.fetchall()

    if not rows:
        time.sleep(1)
        continue

    for row in rows:
        msg_id = row[0]
        body = row[3]
        print("Consumed:", body)

        cur.execute("SELECT pgmq.delete('events', %s)", [msg_id])
        conn.commit()

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Dockerfile

FROM python:3.11-slim
WORKDIR /app
RUN pip install psycopg2-binary
COPY consumer.py .
CMD ["python", "consumer.py"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# consumer.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pg-consumer
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pg-consumer
  template:
    metadata:
      labels:
        app: pg-consumer
    spec:
      containers:
        - name: consumer
          image: pg-consumer
          imagePullPolicy: IfNotPresent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
docker build -t pg-consumer .
kubectl apply -f consumer.yaml

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ensure all pods are running&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;m-mfkghf4fgk producer % kubectl get pods

NAME                          READY   STATUS    RESTARTS   AGE
pg-consumer-c976b84f8-t84mt   1/1     Running   0          27s
pg-producer-85cd846b4-llj72   1/1     Running   0          92s
postgres-5f9b95c698-pjjnp     1/1     Running   0          20m

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify Logs&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl logs deployment/pg-producer

kubectl logs deployment/pg-consumer

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Optional : PG WEB UI&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;m-mfkghf4fgk producer % kubectl run pgweb --image=sosedoff/pgweb -- \
  --host=postgres \
  --port=5432 \
  --user=xxx \
  --pass=xxx \
  --db=queue_db \
  --ssl=disable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl port-forward pod/pgweb 8081:8081
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit in browser - &lt;em&gt;&lt;a href="http://localhost:8081" rel="noopener noreferrer"&gt;http://localhost:8081&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Run Sample query to see the events&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT * FROM pgmq.q_events LIMIT 20;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyajmtcq0dl5y0z2uu36e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyajmtcq0dl5y0z2uu36e.png" alt=" " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Not Everything Needs to Be Kafka
&lt;/h2&gt;

&lt;p&gt;pgmq is not trying to replace Kafka, and it shouldn’t. It solves a different problem. If you need high-throughput streaming, multiple independent consumers, or large-scale event processing, Kafka is still the right tool.&lt;/p&gt;

&lt;p&gt;But if all you need is a reliable, observable queue for background work, retries, or internal workflows, Postgres is often more than enough. You already run it, you already trust it, and you can see exactly what is happening inside it.&lt;/p&gt;

&lt;p&gt;In many systems, the queue is not the product — it is just plumbing. And for plumbing, simple and boring is usually better than powerful and complex.&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>postgres</category>
      <category>eventdriven</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
