<?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: Erkin Khidirov</title>
    <description>The latest articles on DEV Community by Erkin Khidirov (@erkin_khidirov_e1a1d8dc51).</description>
    <link>https://dev.to/erkin_khidirov_e1a1d8dc51</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%2F3121912%2F4d0905d0-b2a3-4b60-9634-645cda95e93e.png</url>
      <title>DEV Community: Erkin Khidirov</title>
      <link>https://dev.to/erkin_khidirov_e1a1d8dc51</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/erkin_khidirov_e1a1d8dc51"/>
    <language>en</language>
    <item>
      <title>Everything You Need to Know About IPv4, IPv6, Dual Stack, DNS64, and NAT64</title>
      <dc:creator>Erkin Khidirov</dc:creator>
      <pubDate>Wed, 21 Oct 2026 14:01:41 +0000</pubDate>
      <link>https://dev.to/erkin_khidirov_e1a1d8dc51/everything-you-need-to-know-about-ipv4-ipv6-dual-stack-dns64-and-nat64-32p</link>
      <guid>https://dev.to/erkin_khidirov_e1a1d8dc51/everything-you-need-to-know-about-ipv4-ipv6-dual-stack-dns64-and-nat64-32p</guid>
      <description>&lt;p&gt;When you start diving into server and network configuration, you constantly run into terms like IPv4, IPv6, NAT, CGNAT, Dual Stack, DNS64, NAT64, and 464XLAT. All too often, they are explained in a way that makes zero sense without a degree in network engineering. Let’s break down from scratch and in plain English how internet addresses work, why we ran out of the old ones, how carrier-grade translators operate under the hood, and the exact places where your backend code might crash with an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why We Ran Out of Old IPv4 Addresses
&lt;/h2&gt;

&lt;p&gt;For computers, phones, and servers to find each other on the internet and exchange data, every device needs its own digital identifier an IP address.&lt;/p&gt;

&lt;p&gt;In 1981, the fourth version of the Internet Protocol, called IPv4, was adopted. This protocol allocated 32 bits of memory for each device’s address. That limit allows for exactly 4,294,967,296 unique addresses (just over four billion). Back in the 1980s, computers only existed in universities and military labs, so four billion numbers seemed like they would last forever.&lt;/p&gt;

&lt;p&gt;Today, however, there are eight billion people on the planet. Almost everyone has a smartphone, homes have routers, laptops, and smart TVs, and data centers run hundreds of millions of servers. As a result, the world physically ran out of unallocated IPv4 addresses back in 2011.&lt;/p&gt;

&lt;h2&gt;
  
  
  Public vs. Private Addresses and How NAT Works
&lt;/h2&gt;

&lt;p&gt;When free addresses started running out, Internet Service Providers (ISPs) could no longer hand out a dedicated, globally routable IP address to every home computer and phone. To solve this problem, addresses were split into two types that developers refer to as public (external, or "white" in Russian networking slang) and private (internal/local, or "gray").&lt;/p&gt;

&lt;p&gt;Imagine a large office building. The building itself has an official street address say, 5 Lenin Street. Any courier in the city can find this building on a map and deliver a package there. In computer networking, a real, globally unique address on the internet like this is called a public IP address.&lt;/p&gt;

&lt;p&gt;Inside this office building, there are a thousand rooms numbered 101 to 1100. If you order food delivery and only put "Room 105" as the address without specifying the street and building number, the courier will never reach you because every other building in town has a Room 105. In networking, these internal room numbers are called private (or local) IP addresses. They are free, you can hand out as many of them as you want, and they only work inside a single building (within a single local network) the wider internet has no idea they exist.&lt;/p&gt;

&lt;p&gt;So how do people sitting in those offices order goods from the internet if they only have internal private numbers?&lt;/p&gt;

&lt;p&gt;To make this work, the building places a receptionist with a notepad at the front entrance. In networking, the software that acts as this receptionist on a router is called NAT (Network Address Translation). It works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An employee in Room 105 wants to open Google and sends a request.&lt;/li&gt;
&lt;li&gt;The receptionist at the entrance (the router running NAT) takes this request, erases the return address "Room 105" from the envelope, and replaces it with the street address of the entire building: "5 Lenin Street."&lt;/li&gt;
&lt;li&gt;At the same time, the receptionist jots down a note in their ledger: "Request #1 to Google was sent by the person in Room 105."&lt;/li&gt;
&lt;li&gt;Google’s server receives the request, sees the building's street address, and sends the response back to "5 Lenin Street."&lt;/li&gt;
&lt;li&gt;The receptionist at the entrance catches the response from Google, checks their notepad, sees that Room 105 is waiting for this specific reply, and forwards the data inside to the right room.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This exact same miniature NAT receptionist is running right now inside your home router so that your phone, laptop, and TV can all access the internet through a single cable.&lt;/p&gt;

&lt;p&gt;When ISPs themselves started running out of addresses, they placed one giant receptionist at the level of an entire neighborhood or city. This massive ISP-level translator is called CGNAT (Carrier-Grade NAT). An ISP takes just a single real public IPv4 address and puts anywhere from 1,000 to 5,000 different apartments behind it. All of these thousands of people sit on internal private addresses and access the internet through one shared external ISP address.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Problems Caused by ISP CGNAT
&lt;/h2&gt;

&lt;p&gt;Putting thousands of different users behind a single shared ISP address solved the address shortage, but it created three major headaches for users and web developers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Getting blocked because of your ISP neighbors. Because a thousand different apartments access the internet under the exact same public IPv4 address, to external websites all these people look like a single person. If just one of those thousand subscribers runs an aggressive web-scraping script, starts sending spam, or catches malware, security systems like Cloudflare or Google will block that external IP address. As a result, the other 999 regular people on the same ISP get an access denied error or get stuck solving endless CAPTCHAs whenever they try to open a website.&lt;/li&gt;
&lt;li&gt;Inability to connect two devices directly (for video calls and gaming). When you simply open a website, your computer messages the server first, so the NAT receptionist writes an entry in their notepad and knows where to send the reply. However, some tasks require two client computers to connect directly to each other (peer-to-peer) without a central server in the middle.&lt;/li&gt;
&lt;li&gt;The first example is browser-based video calls powered by WebRTC. When two people are on a video call on a website, routing a heavy video stream through the website owner's server is far too expensive. Instead, browsers use built-in WebRTC technology to stream audio and video directly from one caller's phone to the other's.&lt;/li&gt;
&lt;li&gt;The second example is co-op multiplayer games. In many games, developers don't rent dedicated servers for matches. Instead, one player's computer becomes the host for the current game lobby, and the other players' computers try to connect directly to that host PC.&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9unhdfcrllbovyy36s8c.jpeg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9unhdfcrllbovyy36s8c.jpeg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Now look at what happens when both people are sitting behind ISP CGNAT. The first person's computer tries to knock on the second person's computer and sends a packet to the shared external address of their ISP. The packet arrives at the ISP's CGNAT receptionist. The receptionist opens their notepad and sees that nobody inside the building ordered this package. With five thousand different apartments behind them, the receptionist has no idea which of the five thousand subscribers this incoming call is meant for so they simply drop the packet. The direct connection fails. For the video call or game to work at all, the developer has to pay out of pocket to rent an intermediate relay server (called a TURN server in WebRTC) that both clients connect to outbound, routing all traffic through it.&lt;/li&gt;
&lt;li&gt;Paid IP addresses at hosting providers. Because there are no free IPv4 addresses left, cloud and hosting providers buy them from one another at prices ranging from $35 to $50 per address. That is why, when you rent a cloud server today, hosting companies charge developers a separate monthly fee just for having a single public IPv4 address attached to the server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why IPv6 Was Created and Why Everyone Hasn't Switched Yet
&lt;/h2&gt;

&lt;p&gt;The sixth version of the Internet Protocol (IPv6) was designed to get rid of ISP CGNAT translators entirely and once again give every smartphone, router, and server in the world its own dedicated public IP address.&lt;/p&gt;

&lt;p&gt;Instead of 32 bits per address, IPv6 allocates 128 bits. That yields exactly 340,282,366,920,938,463,463,374,607,431,768,211,456 unique addresses. With a supply that massive, there is no longer any need to conserve numbers, which is why hosting providers hand out massive blocks of IPv6 addresses for free.&lt;/p&gt;

&lt;p&gt;So why hasn't the entire world shut off the old IPv4 protocol yet?&lt;/p&gt;

&lt;p&gt;The main reason is that the creators of IPv6 made it completely backward-incompatible with the old IPv4 protocol. It is like two people where one only speaks Russian and the other only speaks Chinese. Without a translator, they won't understand a single word. Similarly, a computer configured only with the old IPv4 protocol physically cannot send data to a server that only has a new IPv6 address, and vice versa.&lt;/p&gt;

&lt;p&gt;This incompatibility caused the rollout delay. If you disable old IPv4 on your website right now and leave only IPv6, not a single user whose home ISP still runs exclusively on the old protocol will be able to reach your site.&lt;/p&gt;

&lt;p&gt;And many residential ISPs (especially in the CIS and Central Asia) are in no rush to enable IPv6 for a simple financial reason. An ISP's network is built out of expensive enterprise-grade core routers that pump traffic between districts and cities. On older hardware, IPv6 support often performs poorly or requires costly paid licenses and a complete reconfiguration of the company's network. It makes no financial sense for an ISP to spend money on this when they have already hidden thousands of subscribers behind a single address for free using CGNAT, and all popular websites still load just fine over legacy IPv4.&lt;/p&gt;

&lt;p&gt;The companies that switched to the new protocol first were those where the old setup simply buckled under the load. These include major cloud hosting providers and massive mobile carriers (for example, in Germany, France, the US, and India), where tens of millions of smartphones were literally overflowing the state tables and memory of CGNAT gateways. As a result, more than half of global mobile traffic today already runs over IPv6, while fixed-line home internet in many countries continues to run on IPv4.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Dual Stack?
&lt;/h2&gt;

&lt;p&gt;Because half the internet sits on the old protocol and half of all mobile networks have already moved to the new one, website and server owners have had to support both protocols at the same time. This operating mode is called Dual Stack.&lt;/p&gt;

&lt;p&gt;There is no translation from one protocol to another inside Dual Stack. Continuing our language analogy, a Dual Stack server is simply a bilingual person who speaks both Russian and Chinese fluently.&lt;/p&gt;

&lt;p&gt;When you configure Dual Stack on your server, it looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your server’s network interface is assigned two public addresses at once (one legacy IPv4 address and one new IPv6 address).&lt;/li&gt;
&lt;li&gt;In the internet’s phonebook (the DNS system, which translates a human-readable domain name like example.com into a numeric IP address), you create two records for your domain. An A record stores your old IPv4 address, and an AAAA record stores your new IPv6 address.&lt;/li&gt;
&lt;li&gt;Your web server (such as Nginx or a Go application) listens for incoming connections on both addresses simultaneously. If a user connects from a home internet connection in Tashkent that only has IPv4, the server talks to them over IPv4. If a user connects from a mobile network in Germany running IPv6, that exact same server replies to them directly over IPv6.&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgsdi3fnajv2vkw9zozel.jpeg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgsdi3fnajv2vkw9zozel.jpeg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Meanwhile, modern browsers and smartphones run a built-in fallback mechanism called Happy Eyeballs. When a user opens your site, the browser requests both addresses (A and AAAA) from DNS and tries connecting over the newer IPv6 protocol first. If for some reason the ISP’s IPv6 routing is broken and the server doesn't respond within 150 to 250 milliseconds, the browser seamlessly falls back to the old IPv4 address without the user ever noticing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are DNS64 and NAT64, and Why Are They Needed?
&lt;/h2&gt;

&lt;p&gt;Maintaining Dual Stack on a single server is easy. However, inside a massive mobile carrier (like T-Mobile in Germany) with tens of millions of smartphones connected simultaneously, running two parallel networks at once is far too complex and expensive.&lt;/p&gt;

&lt;p&gt;So mobile carriers took a different path: they completely turned off the old IPv4 protocol on their subscribers' phones and left smartphones with IPv6-only addresses.&lt;/p&gt;

&lt;p&gt;At this point, any web developer will ask a practical question: Suppose I created a subdomain for my site, ws.example.com, that handles WebSocket connections, and I only configured an old IPv4 address (A record) for it in DNS without setting up an IPv6 address. Will a user in Germany on an IPv6-only phone be able to connect to my WebSocket?&lt;/p&gt;

&lt;p&gt;Yes, they will 100% of the time.&lt;/p&gt;

&lt;p&gt;First, if you have Cloudflare protection enabled in front of your server (the orange cloud icon is turned on in your DNS dashboard), Cloudflare automatically acts as a Dual Stack proxy on your behalf. It gives the user in Germany its own IPv6 address, accepts their connection, and then forwards the data to your server over your old IPv4 address.&lt;/p&gt;

&lt;p&gt;Second, even if you disabled Cloudflare proxying and pointed traffic directly to your server with an old IPv4 address, the German user will still connect. Their connection is automatically bridged by a pair of translators DNS64 and NAT64 installed inside their mobile carrier's network.&lt;/p&gt;

&lt;p&gt;The number 64 in these names has nothing to do with 64-bit processors. In networking, it stands for "from version 6 to version 4" meaning traffic translation from the IPv6 protocol to the IPv4 protocol.&lt;/p&gt;

&lt;p&gt;Why are two separate programs (both DNS64 and NAT64) needed here? Because the IPv4 protocol is completely disabled on the user's smartphone. If you simply handed that phone your server's old IPv4 address (say, 185.10.20.30), the phone's operating system wouldn't understand the format and would immediately throw a network error without even attempting to send a packet anywhere.&lt;/p&gt;

&lt;p&gt;So the mobile carrier tricks the smartphone in two steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step one is handled by the DNS64 server (address synthesis in the phonebook). When the browser on the smartphone tries to connect to ws.example.com, it asks the carrier’s DNS64 server for a new-style IPv6 address (AAAA record). The DNS64 server checks your domain, sees that you don't have an IPv6 address, and finds only your old IPv4 address (185.10.20.30). To prevent the phone from throwing an error, DNS64 takes a special standardized IPv6 prefix (the translator prefix, typically 64:ff9b::/96) and glues your old 185.10.20.30 address onto the very end of it. This produces a synthetic, yet validly formatted IPv6 address, which DNS64 returns to the smartphone.&lt;/li&gt;
&lt;li&gt;Step two is handled by the NAT64 router (packet translation at the network edge). The smartphone sees a normal-looking IPv6 address and happily sends packets to it. Inside the mobile carrier's network, a routing rule directs all packets starting with that synthetic prefix to a dedicated border router called NAT64. This router catches the packet from the phone, strips off the glued-on IPv6 header, extracts your real 185.10.20.30 address from the tail end, repacks the payload into a standard legacy IPv4 packet, and sends it out to your server. When your server replies, the NAT64 router wraps the response back into an IPv6 envelope and sends it back to the phone.&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz0m171og661fmqsb8fjy.jpeg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz0m171og661fmqsb8fjy.jpeg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here it is crucial to understand the golden rule: The DNS64 and NAT64 translators sit inside the client’s mobile carrier network and only work one way when a device inside an IPv6 network initiates an outbound call to an old IPv4 server.&lt;/p&gt;

&lt;p&gt;Standard cloud hosts (like Hetzner or DigitalOcean) do not provide an inbound translator for you. If you decide to save a couple of dollars when renting a server by unchecking the public IPv4 address option and buying an IPv6-only server, users on legacy home internet connections will not be able to reach your server directly (you would have to put a Cloudflare proxy in front of it). Even worse, from an IPv6-only server, you won't even be able to run git clone from GitHub, because GitHub's servers still run exclusively on legacy IPv4. For an IPv6-only server to download packages from legacy IPv4 sites, you have to manually configure a free public DNS64 resolver (such as the free one provided by Cloudflare) in your server's DNS settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are 464XLAT and CLAT?
&lt;/h2&gt;

&lt;p&gt;The DNS64 + NAT64 combo saves the day whenever a browser or app connects to your server using a domain name (like ws.example.com). After all, it is during that DNS lookup that the DNS64 server gets the chance to quietly slip the phone a synthetic IPv6 address.&lt;/p&gt;

&lt;p&gt;But what happens if an app on the phone skips DNS entirely and tries to connect to a server directly using a raw numeric IPv4 address (like 185.10.20.30)?&lt;/p&gt;

&lt;p&gt;In software development, this happens in two main scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First, legacy apps and games where programmers hardcoded the server’s raw IPv4 address directly into the source code.&lt;/li&gt;
&lt;li&gt;Second, our familiar friend WebRTC for video calls, where during connection setup (ICE candidate exchange), browsers and servers swap raw numeric IP addresses with each other without using domain names at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When an app on an IPv6-only smartphone tries to send a packet directly to the raw numbers 185.10.20.30, the DNS64 server never even hears about it because nobody queried the DNS phonebook. The phone’s operating system sees an attempt to send a legacy-format packet and drops the connection immediately.&lt;/p&gt;

&lt;p&gt;To keep video calls and games from breaking on mobile networks, engineers invented 464XLAT. The numbers 4, 6, and 4 stand for translating every packet twice along the path "from IPv4 to IPv6, and back to IPv4" while XLAT is short for translation.&lt;/p&gt;

&lt;p&gt;Here is how it works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Built directly into the operating system of every modern smartphone (both Android and iOS) is a tiny system-level translator daemon called CLAT (Customer-side Translator).&lt;/li&gt;
&lt;li&gt;When CLAT detects that the mobile carrier only gave the phone an IPv6 connection, it spins up a virtual (fake) legacy network interface right inside the phone's memory and assigns it a local private IPv4 address.&lt;/li&gt;
&lt;li&gt;The app or WebRTC video engine sees what looks like a working IPv4 internet connection on the phone and sends a packet to your server’s raw address: 185.10.20.30.&lt;/li&gt;
&lt;li&gt;Right on the phone's CPU, the CLAT program intercepts this packet, repackages it into an IPv6 packet, and sends it out over the carrier’s mobile network (this is the first translation: from version 4 to version 6).&lt;/li&gt;
&lt;li&gt;At the edge of the mobile carrier's network, this packet is caught by our familiar carrier border router, NAT64 (also called PLAT, or Provider-side Translator, in 464XLAT documentation), which converts the packet back into the old IPv4 format and delivers it to your server (this is the second translation: from version 6 to version 4).&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2fxf0xsq3j2f21bt7eq3.jpeg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2fxf0xsq3j2f21bt7eq3.jpeg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Is the Practical Takeaway for a Developer?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Thanks to the safety nets of DNS64, NAT64, and 464XLAT, a mobile user will reach your server and connect to your socket even if you only have legacy IPv4 configured.&lt;/p&gt;

&lt;p&gt;However, going through translators always comes with an overhead cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With 464XLAT, every single voice or video packet is reassembled twice (first by the phone's CPU, then by the carrier's router), which adds latency.&lt;/li&gt;
&lt;li&gt;Long-lived WebSocket connections running through a carrier's NAT64 router can drop out of nowhere. Because a mobile carrier’s NAT64 router stores a state table of all active subscriber connections in its RAM, it aggressively purges idle connections to keep its memory from overflowing often killing any connection that hasn't transferred data for 30 to 60 seconds. If nobody types anything in your chat room for half a minute, the carrier's translator will silently sever the connection. To prevent this, you must either send empty keepalive Ping and Pong frames over the WebSocket every 15 to 20 seconds, or enable native IPv6 on your server so mobile clients connect directly, bypassing all translators.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why a Hetzner IPv6 Address Won't Paste into Cloudflare
&lt;/h2&gt;

&lt;p&gt;Let’s look at a classic mistake developers make when setting up IPv6 for the first time, using a real-world example with Hetzner Cloud and the Cloudflare DNS dashboard.&lt;/p&gt;

&lt;p&gt;When you rent a server on Hetzner, the host gives you one single legacy IPv4 address and an entire block of new IPv6 addresses. In the Hetzner control panel, this block is displayed as four groups of characters separated by colons, ending with a double colon and /64 (for example, 2a01:4f8:c010:d56::/64).&lt;/p&gt;

&lt;p&gt;If you copy that entire string and try to paste it into an AAAA record in Cloudflare, you will get an error saying that the IPv6 address is invalid.&lt;/p&gt;

&lt;p&gt;This happens because a string ending in /64 is not the address of a single specific server it is the notation for your entire allocated subnet (essentially an entire street containing 18 quintillion individual addresses).&lt;/p&gt;

&lt;p&gt;Every 128-bit IPv6 address is always split cleanly in half:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The first 64 bits (the first four groups of hex characters) represent the identifier of your network on Hetzner's routing equipment.&lt;/li&gt;
&lt;li&gt;The second 64 bits (everything that comes after the double colon ::) represent the specific device (interface) identifier inside your network. Two colons in a row (::) simply compress a long string of zeros to make the address shorter to write.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cloudflare’s DNS settings won't accept the address of an entire street with a /64 suffix; they require the exact house number of a single server. When Hetzner automatically provisions your server, it always assigns the very first address from your allocated block to your server's network interface. Therefore, to add an AAAA record in Cloudflare, all you need to do is delete the /64 at the end of the string and put the number 1 right after the double colons (for example, 2a01:4f8:c010:d56::1).&lt;/p&gt;

&lt;h2&gt;
  
  
  Four Places in Code and Server Configs Where Developers Make Mistakes
&lt;/h2&gt;

&lt;p&gt;As a web developer, you don't need to configure network translators yourself ISPs and services like Cloudflare handle that. However, the moment you enable Cloudflare for your site or add an AAAA record in DNS, your backend will start receiving requests from users with IPv6 addresses. Here are four specific places you need to audit in your project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database column length for IP addresses. If you store user IP addresses in your database (for instance, during registration, checkout, or in login audit logs), check the column width. In the past, developers often used VARCHAR(15) because an old IPv4 address is never longer than 15 characters. However, a full IPv6 address can be up to 45 characters long. If your database column is capped at 15 characters, the very first mobile user who logs into your site will trigger a database truncation/overflow error and won't be able to sign in. Always set your column length to VARCHAR(45) or use the dedicated INET data type in PostgreSQL.&lt;/li&gt;
&lt;li&gt;Parsing IP addresses and ports in code. In the old IPv4 protocol, the IP address was separated from the port number by a colon (e.g., 185.10.20.30:8080), so many programmers simply split the string on the colon character using a standard string split(":") function. In IPv6, the address itself contains multiple colons. If you run a naive split(":") on an IPv6 address, your code will break and grab a chunk of the IP address instead of the port number. To avoid this bug entirely, never parse network addresses manually with string functions; always use the built-in functions from your programming language's standard networking library (such as net.SplitHostPort and net.JoinHostPort in Go's standard net package).&lt;/li&gt;
&lt;li&gt;Rate limiting and bot blocking. Many websites protect themselves with a rate limiter that allows no more than, say, 10 requests per second from a single IP address. With legacy IPv4, we simply take the entire address and use it as the key in our rate-limiting counter. In the IPv6 world, however, blocking a single specific IP address is useless. As we saw with Hetzner, IPv6 networks don't hand out a single address to a server or home router they hand out an entire /64 subnet containing 18 quintillion free addresses. A spammer can easily send every new request from the next sequential address inside their free subnet just by rotating the last few digits. For your rate limiter to work properly, whenever you receive an incoming IPv6 address, you must programmatically mask/strip off the second half (the last 64 bits) and count all requests coming from the first 64 bits (/64 prefix) as belonging to the exact same user.&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmm5161r1v05o7729vxem.jpeg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmm5161r1v05o7729vxem.jpeg" alt=" " width="800" height="597"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open ports in the second firewall. When your server runs in Dual Stack mode, it effectively has two separate front doors. On Linux, the classic iptables firewall utility only closes ports for the old IPv4 protocol, while a completely separate utilityip6tablesgoverns the IPv6 protocol. This frequently leads to a dangerous security blind spot: a developer spins up a database or Redis instance on a server, locks down the port from the outside world using familiar iptables, scans the server externally via its old IPv4 address, sees that the port is closed, and assumes the server is secure. Meanwhile, that database is sitting wide open to the entire internet through the second front door via its public IPv6 address because nobody configured any rules in ip6tables. If you use a modern firewall wrapper like ufw, it automatically generates rules for both protocols at once but you should always test your closed ports from the outside using both your legacy IPv4 address and your new IPv6 address.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Let's get back to my project, shomla, and wrap up this entire story with the practical takeaways. Once I dug into the mechanics of how ISP translators work, it became clear why users on mobile internet in Germany were able to join my voice chat without any issues even when my server only had the old IPv4 protocol configured. Behind the scenes, Cloudflare proxy servers and carrier NAT64 gateways paired with the built-in CLAT daemon inside the smartphones themselves were quietly doing all the heavy lifting of translating packets for me.&lt;/p&gt;

&lt;p&gt;However, understanding how this hidden machinery actually works allowed me to eliminate critical bottlenecks in my chat architecture. It is one thing when you run a standard content website with articles, where an extra packet reassembly step on a mobile carrier's router makes no noticeable difference. It is a completely different story when you are running a real-time voice chat built on WebSockets and WebRTC.&lt;/p&gt;

&lt;p&gt;When a mobile user's traffic goes through NAT64 and 464XLAT translators, their smartphone and the carrier's hardware are forced to reassemble every single voice packet twice on the fly, while the carrier's router constantly tries to forcibly kill the WebSocket connection during any brief pause in data transmission. As soon as I properly added my server's IPv6 address (ending in ::1) to the AAAA DNS records for my domains, configured my server to listen on both protocols, and verified that keepalive Ping and Pong frames were firing regularly over the WebSockets, mobile clients were able to connect to the server directly over pure, native IPv6 with zero middlemen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I Recommend Doing on Your Project Right Now&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Don't drop everything and panic if your server is currently running only on a legacy IPv4 address. Your website is already accessible to users worldwide because mobile carriers and Cloudflare have taken care of backward compatibility for you.&lt;/li&gt;
&lt;li&gt;If your hosting provider (such as Hetzner or DigitalOcean) has already given your server a free IPv6 subnet, spend ten minutes enabling full Dual Stack. Replace the /64 suffix at the end of your allocated address with 1, add an AAAA record in your DNS dashboard, and make sure your Nginx proxy or backend application is listening on both network interfaces. Doing so will spare your mobile users from double packet translation and random connection drops on carrier routers.&lt;/li&gt;
&lt;li&gt;Audit your backend code and server configuration against the four pitfalls we covered above. Expand your IP address database column to at least 45 characters (or INET), remove manual colon-based string splitting when parsing addresses in code, update your spam blockers and rate limiters to group IPv6 users by their /64 prefix (the first 64 bits), and check your server's ip6tables firewall rules today so your database isn't accidentally left wide open to the entire internet.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ipv4</category>
      <category>ipv6</category>
      <category>dns64</category>
      <category>dualstack</category>
    </item>
    <item>
      <title>Installation and configuration of HermitMQ</title>
      <dc:creator>Erkin Khidirov</dc:creator>
      <pubDate>Wed, 19 Aug 2026 14:05:41 +0000</pubDate>
      <link>https://dev.to/erkin_khidirov_e1a1d8dc51/installation-and-configuration-of-hermitmq-22lp</link>
      <guid>https://dev.to/erkin_khidirov_e1a1d8dc51/installation-and-configuration-of-hermitmq-22lp</guid>
      <description>&lt;p&gt;I will show you how to deploy HermitMQ and integrate clients. The broker comes as a ready package for Debian and Ubuntu systems. Download the latest release and install it using the package manager. The service will start automatically.&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;cd&lt;/span&gt; /tmp/
wget https://github.com/ekhidirov/hermitmq/releases/latest/download/hermitmq_amd64.deb
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ./hermitmq_amd64.deb
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status hermitmq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Client installation
&lt;/h2&gt;

&lt;p&gt;To work with the broker download the SDK into your project. The import paths are now configured correctly for public access.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/ekhidirov/hermitmq/pkg/client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Project structure
&lt;/h2&gt;

&lt;p&gt;The Go compiler forbids keeping two entry points in one folder. I placed the files in different directories inside the cmd folder. This solves all compilation problems. Prepare the following file structure before writing code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my_project/
├── cmd/
│   ├── consumer/
│   │   └── main.go
│   └── producer/
│       └── main.go
├── go.mod
└── go.sum
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Interactive sender
&lt;/h2&gt;

&lt;p&gt;First we initialize the producer. When connecting we specify the server address topic and partition number. Instead of a placeholder be sure to specify the real IP address where your broker is deployed. I added a loop to read keyboard input. The program will wait for your text and send it to the broker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"bufio"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"log"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/ekhidirov/hermitmq/pkg/client"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Connect to the broker replacing YOUR_SERVER_IP with actual IP&lt;/span&gt;
    &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewProducer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"YOUR_SERVER_IP:9092"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"test_topic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Connection error: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Connected to the cloud! Type your messages and press Enter:"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// Start the command line scanner&lt;/span&gt;
    &lt;span class="n"&gt;scanner&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;bufio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewScanner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stdin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;scanner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;scanner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt; &lt;span class="c"&gt;// Ignore empty Enter key presses&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Send error: %v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"✔ Delivered (Offset: %d)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;scanner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Console read error: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Reading and committing
&lt;/h2&gt;

&lt;p&gt;Listener configuration is done similarly. I create a consumer and pass the group name. The broker itself will remember the reading position. The receive method works by blocking the goroutine. I trigger a manual commit only after the text is successfully printed to the screen to guarantee delivery.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"log"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/ekhidirov/hermitmq/pkg/client"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Connect to the broker replacing YOUR_SERVER_IP with actual IP&lt;/span&gt;
    &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"YOUR_SERVER_IP:9092"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"test_group"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"test_topic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Connection error: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Listening to the network. Waiting for new messages..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Receive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Network error or disconnection: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c"&gt;// Print the received payload&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[Incoming | Offset: %d] %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payload&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="c"&gt;// Commit the read offset&lt;/span&gt;
        &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Launching the integration
&lt;/h2&gt;

&lt;p&gt;Open two terminal windows. In the first one start the listener. In the second one start the sender and write messages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go run cmd/consumer/main.go
go run cmd/producer/main.go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>hermitmq</category>
      <category>go</category>
      <category>devops</category>
    </item>
    <item>
      <title>How I wrote a Go message broker with a throughput of a million messages per second</title>
      <dc:creator>Erkin Khidirov</dc:creator>
      <pubDate>Tue, 18 Aug 2026 21:15:58 +0000</pubDate>
      <link>https://dev.to/erkin_khidirov_e1a1d8dc51/how-i-wrote-a-go-message-broker-with-a-throughput-of-a-million-messages-per-second-o86</link>
      <guid>https://dev.to/erkin_khidirov_e1a1d8dc51/how-i-wrote-a-go-message-broker-with-a-throughput-of-a-million-messages-per-second-o86</guid>
      <description>&lt;p&gt;I built HermitMQ entirely in Go. The main feature is ditching heavy wrappers like JSON in favor of a custom 29 byte binary protocol. Additionally, data transmission over the network uses a direct file to socket copy mechanism. I will go into detail about the architecture, data storage approaches, benchmark numbers, and show how it is implemented in code.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The full source code for the HermitMQ project is available on GitHub:&lt;/em&gt;    &lt;a href="https://github.com/ekhidirov/hermitmq" rel="noopener noreferrer"&gt;https://github.com/ekhidirov/hermitmq&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with standard brokers and the cost of serialization
&lt;/h2&gt;

&lt;p&gt;When the message counter exceeds hundreds of thousands per second, the main problem for a Go developer is the garbage collector. If every message is parsed via standard JSON, the application starts allocating a massive number of small objects in memory. The GC wakes up too frequently, eating up CPU time and causing network latency spikes.&lt;/p&gt;

&lt;p&gt;To avoid triggering the garbage collector at every turn, I completely abandoned standard serialization libraries. Every message is packed into a custom header of exactly 29 bytes. In code, the message structure looks extremely simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Magic&lt;/span&gt;       &lt;span class="kt"&gt;byte&lt;/span&gt;
    &lt;span class="n"&gt;Timestamp&lt;/span&gt;   &lt;span class="kt"&gt;uint64&lt;/span&gt;
    &lt;span class="n"&gt;Offset&lt;/span&gt;      &lt;span class="kt"&gt;uint64&lt;/span&gt;
    &lt;span class="n"&gt;KeySize&lt;/span&gt;     &lt;span class="kt"&gt;uint32&lt;/span&gt;
    &lt;span class="n"&gt;PayloadSize&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;
    &lt;span class="n"&gt;RecordCount&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;
    &lt;span class="n"&gt;Key&lt;/span&gt;         &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;
    &lt;span class="n"&gt;Payload&lt;/span&gt;     &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first byte is a magic number for version checking and instantly discarding bad packets. Next come 8 bytes for the timestamp in nanoseconds and 8 bytes for the offset, which the broker fills in itself to maintain message order. Then come the key and payload sizes, 4 bytes each. Finally, 4 bytes are reserved for the record count to support batching. The broker reads the stream using the binary package and reuses buffers via sync.Pool. As a result, under standard loads, we achieve practically zero memory allocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Being honest about allocations and plans for zero serialization
&lt;/h2&gt;

&lt;p&gt;To be completely honest: although the broker is incredibly frugal under standard loads, a memory management compromise still remains.&lt;/p&gt;

&lt;p&gt;An absolute victory over allocations and the GC has been achieved on the side of serving data to consumers, thanks to io.CopyN (the very zero copy approach I will discuss below). However, on the receiving end from producers, the Decode method still dynamically allocates slices for the payload size (make([]byte, m.PayloadSize)).&lt;/p&gt;

&lt;p&gt;In future releases, I plan to completely eliminate this structure. We will move towards the concept of zero serialization (like in flatbuffers) and micro optimizations with branchless logic in hot loops. The broker will access byte offsets directly in the network buffer, without unpacking them into Go structs at all. This will finally eliminate the GC from the message processing path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Direct data transfer at the kernel level
&lt;/h2&gt;

&lt;p&gt;Copying data from the OS kernel into user space and back to the network interface is very expensive. It is double the work for the CPU and RAM. In HermitMQ, consumers receive data directly.&lt;/p&gt;

&lt;p&gt;When a client requests data, the broker looks up the offset in the log file. Instead of reading the file into the program memory, it uses the built in io.CopyN function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;bytesToSend&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HeaderSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keySize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payloadSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewSectionReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;walFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytesToSend&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CopyN&lt;/span&gt;&lt;span class="p"&gt;(&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;reader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytesToSend&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Data&lt;/span&gt; &lt;span class="n"&gt;flows&lt;/span&gt; &lt;span class="n"&gt;straight&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;physical&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;disk&lt;/span&gt; &lt;span class="n"&gt;into&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;consumer&lt;/span&gt; &lt;span class="n"&gt;network&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Latencies&lt;/span&gt; &lt;span class="n"&gt;remain&lt;/span&gt; &lt;span class="n"&gt;minimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;CPU&lt;/span&gt; &lt;span class="n"&gt;does&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="n"&gt;strain&lt;/span&gt; &lt;span class="n"&gt;itself&lt;/span&gt; &lt;span class="n"&gt;moving&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="n"&gt;around&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Data storage, mmap, and compaction
&lt;/h2&gt;

&lt;p&gt;All data is written to a write ahead log (WAL). The log is divided into segments. Each segment has a .wal file with raw data and an .idx file to map offsets to physical bytes on the hard drive.&lt;/p&gt;

&lt;p&gt;For fast searching, indices are mapped directly into RAM via mmap. The broker performs a binary search in memory and instantly finds the required byte. Background log compaction is also implemented here. It deduplicates messages by key, leaving only the most relevant data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Surviving crashes: the torn page recovery mechanism
&lt;/h2&gt;

&lt;p&gt;Any database developer knows: a server can crash at any millisecond due to a power failure or a hard kill 9 signal. If this happens during a log write, a half written chunk of data (a torn page) will be left at the end of the file.&lt;/p&gt;

&lt;p&gt;HermitMQ implements an automatic recovery mechanism. On startup, the broker sequentially scans the WAL files. If it sees that the expected message size in the header exceeds the physical size of the file on disk, it understands that the write was interrupted. Instead of panicking and crashing the entire segment, the broker safely truncates this corrupted tail at the hardware level using os.Truncate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;currentPos&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;msgSize&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;totalFileSize&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PARTIAL MESSAGE DETECTED. Truncating tail."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Truncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;walPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentPos&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This guarantees that the broker will boot up with a completely consistent state, even after the most severe operating system crash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lock sharding and scaling
&lt;/h2&gt;

&lt;p&gt;Storing consumer group offsets under a single global mutex is a bad idea when dealing with thousands of connections. Threads will start queuing up and kill all performance. Therefore, the offset store is split into 256 independent shards.&lt;/p&gt;

&lt;p&gt;The broker takes a string key, runs it through a hashing algorithm, and routes it to the correct shard. Here is the implementation of this hash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;getShardIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;uint8&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2166136261&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="m"&gt;16777619&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;uint8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OffsetShardCount&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Incoming requests are evenly spread across the allocated memory. Producers and consumers do not block each other, and the broker scales perfectly across CPU cores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer experience: on demand routing
&lt;/h2&gt;

&lt;p&gt;Working with hardcore systems is often painful due to excessive infrastructure bureaucracy, you usually have to hit an API in advance to configure topics and partitions. I solved this problem through on demand routing.&lt;/p&gt;

&lt;p&gt;If a producer or consumer knocks on the broker and requests a non existent topic, the broker does not reject the connection with an error. Instead, it transparently creates the required folder and file structure on the fly under an RWMutex lock.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RLock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;topics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;topicName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RUnlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"auto-creating new topic on demand"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"topic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topicName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateTopic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topicName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client simply needs to start writing data, the entire physical structure under the hood will spin up on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smart batching on the client
&lt;/h2&gt;

&lt;p&gt;Throughput depends not only on the server but also on the clients. In the project, the test producer uses Go channels and tickers to group messages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ProducersCount&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="n"&gt;MessagesPerWorker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt;
    &lt;span class="n"&gt;BatchSize&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
    &lt;span class="n"&gt;LingerTime&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Millisecond&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Workers buffer messages and flush them to the TCP socket only when two conditions are met: a batch of 100 records has accumulated, or a 100 millisecond wait timer expires. This glues a hundred logical records into a single dense header and a single system write call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark numbers and stress test results
&lt;/h2&gt;

&lt;p&gt;To measure the real throughput of the pipeline, I set up a cluster of 10 parallel producers and a separate consumer. The test scenario pumps 1,000,000 messages through the broker.&lt;/p&gt;

&lt;p&gt;Producers generate 10 threads of 100,000 messages each. The consumer fetches the incoming stream, parses the 29 byte headers, and calculates the net throughput over time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;totalMB&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logicalCount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1812&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Throughput: %.0f msgs/sec (Approx %.0f MB/sec)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logicalCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Seconds&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; 
    &lt;span class="n"&gt;totalMB&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Seconds&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a batch of 1,000,000 messages, the entire volume of data is pumped through in a matter of seconds. Thanks to zero copy and the complete absence of intermediate memory allocations, the broker delivers hundreds of thousands of messages per second and utilizes tens of megabytes of traffic per second on a single instance, bottlenecked solely by the physical speed of the disk bus and the local TCP stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overload protection and security
&lt;/h2&gt;

&lt;p&gt;The broker is protected against memory exhaustion attacks by strict limits. The payload size is strictly validated before memory is allocated; by default, the limit is set to 50 megabytes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;maxBytes&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxPayloadMB&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PayloadSize&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;maxBytes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"security policy violation: payload too large"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the size exceeds the limit or a packet arrives with a bad starting byte, the broker does not crash with an out of memory error; it simply drops the malicious connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Graceful shutdown
&lt;/h2&gt;

&lt;p&gt;High delivery speed is meaningless if data stuck in OS buffers is lost during a restart. That is why the broker knows how to respond correctly to system interrupt signals.&lt;/p&gt;

&lt;p&gt;Interception of SIGINT and SIGTERM signals is implemented in main.go. Upon receiving a signal, the broker stops accepting new connections, closes the TCP listener, and neatly iterates through all topics and partitions. For each open segment, Sync() and Close() are forcibly called, flushing all file descriptors to disk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;sigChan&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sigChan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGTERM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;sigChan&lt;/span&gt;
&lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"received shutdown signal, shutting down..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Writing your own message broker is excellent practice for understanding how high load systems work at a low level. The architecture allows implementing direct memory management, data streaming, and concurrency without unnecessary headaches. Ditching fat frameworks gives full control over every allocated byte and provides predictable performance under any load.&lt;/p&gt;

</description>
      <category>hermitmq</category>
      <category>json</category>
      <category>go</category>
      <category>devops</category>
    </item>
    <item>
      <title>How Messengers Actually Encrypt Messages (End-to-End)</title>
      <dc:creator>Erkin Khidirov</dc:creator>
      <pubDate>Thu, 29 May 2025 22:34:20 +0000</pubDate>
      <link>https://dev.to/erkin_khidirov_e1a1d8dc51/how-messengers-actually-encrypt-messages-end-to-end-5a52</link>
      <guid>https://dev.to/erkin_khidirov_e1a1d8dc51/how-messengers-actually-encrypt-messages-end-to-end-5a52</guid>
      <description>&lt;p&gt;Hi everyone!&lt;/p&gt;

&lt;p&gt;I've written an article and I'm publishing it prematurely. Initially, I planned to write it after the project was completed, but since there are still a couple of months left until the end, I decided not to waste time and write the article while the information is still fresh in my mind. Besides, I'm mostly writing this for myself. :) In one of my latest projects, which I'm developing as open source, I implemented end-to-end encryption—similar to how WhatsApp or Telegram do it, for example.&lt;/p&gt;

&lt;p&gt;In this article, we'll dive into the implementation of client-side message encryption using JavaScript and the Web Crypto API, breaking down a practical example that will be at the very end of the article.&lt;/p&gt;

&lt;p&gt;Let's start by saying that if you're a complete beginner in cryptography, understanding what's written here might not be easy. Even with 10 years of development experience, I had to scratch my head a bit—everything happening here is pure mathematics, which we won't be discussing in this article :) The easily impressed might think it's magic :)&lt;/p&gt;

&lt;p&gt;If I were to briefly explain the essence of end-to-end encryption without complex words and terms:&lt;/p&gt;

&lt;h2&gt;
  
  
  The Magic of Encryption in Three Keys
&lt;/h2&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%2F3sr7j0bqnqb7kdbwk2ok.jpg" 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%2F3sr7j0bqnqb7kdbwk2ok.jpg" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;br&gt;
Three keys are the foundation upon which end-to-end encryption is built. Pay attention to the key in the center – this is important.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The foundation on which everything rests is these three keys. Refer back to this section if something is unclear.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;Private Key: Stored (in encrypted form).&lt;/li&gt;
&lt;li&gt;Public Key: Accessible to everyone.&lt;/li&gt;
&lt;li&gt;Shared Secret / Symmetric Key: Generated based on your private key + your contact's public key. This is the key used for the direct encryption and decryption of messages.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The combination of your private key + your contact's public key allows you to obtain the shared secret key (in our example below, this will be an AES key). Thanks to this shared secret key, you can encrypt and decrypt messages.&lt;/p&gt;

&lt;p&gt;Private and public keys can be stored in a database, but there's a nuance with the private key. The private key itself is not recommended to be stored in plain text; it needs to be additionally encrypted with the user's password or any other keyword (within a messenger, this is typically the user's password). We store the public key in plain text.&lt;/p&gt;

&lt;p&gt;The shared secret key (the one referred to as this.aesKey in the code below) is not stored in the database. It is generated (calculated) each time a chat with a specific contact is initialized. This might cause confusion: how will we decrypt messages if this key is not stored but generated anew? This is where the "magic" of asymmetric encryption and key exchange protocols lies.&lt;/p&gt;

&lt;p&gt;The shared key is "Your private key" + "Contact's public key."&lt;/p&gt;

&lt;p&gt;When you open a chat with a contact, your client recalculates this shared secret key, as you already know, like this: (Your private key + Your contact's public key = Shared secret key). With this key, you encrypt new messages and decrypt all previous messages in this chat, as they were encrypted with the same shared secret key. You can wrack your brains for a long time to no avail until you understand asymmetric encryption and key exchange protocols.&lt;/p&gt;
&lt;h2&gt;
  
  
  Asymmetric Encryption and ECDH
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Asymmetric encryption&lt;/strong&gt; uses a pair of keys: public and private. The public key can be freely distributed, while the private key must be kept secret by its owner, meaning in an encrypted form.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ECDH (Elliptic Curve Diffie-Hellman)&lt;/strong&gt; is a key exchange protocol based on elliptic curve mathematics. It allows two parties, each possessing their own ECDH key pair (private and public), to establish a shared secret key over an insecure channel. Importantly, a third party, even if they intercept their public keys, cannot compute this shared secret. Our example uses the P-256 curve – a popular and reliable standard.&lt;/p&gt;

&lt;p&gt;I think few understood what they just read. All you need to understand at this stage is that the technology works :) Later, the puzzle will come together, perhaps after a reread. And now, a bit about built-in browser technologies.&lt;/p&gt;
&lt;h2&gt;
  
  
  Web Crypto API
&lt;/h2&gt;

&lt;p&gt;The Web Crypto API is a JavaScript interface built into browsers that provides access to low-level cryptographic primitives. It allows performing operations such as hashing, signature generation, encryption, and decryption. Using the Web Crypto API is preferable to third-party libraries for basic cryptographic operations, as it is often hardware-accelerated and thoroughly vetted for security. All Web Crypto API operations are asynchronous and return a Promise.&lt;/p&gt;

&lt;p&gt;Now let's move on to the practical analysis. I've created a ChatCrypto class, which we'll examine in more detail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class ChatCrypto {

  constructor(myPrivateKeyBase64, theirPublicKeyBase64) {
    this.myPrivateKeyBase64 = myPrivateKeyBase64;
    this.theirPublicKeyBase64 = theirPublicKeyBase64;
    this.aesKey = null; // The shared symmetric AES key will be stored here
  }

  static base64ToArrayBuffer(base64) {
    const binary = atob(base64);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i &amp;lt; binary.length; i++) {
      bytes[i] = binary.charCodeAt(i);
    }
    return bytes.buffer;
  }

  static arrayBufferToBase64(buffer) {
    const bytes = new Uint8Array(buffer);
    let binary = '';
    for (let b of bytes) {
      binary += String.fromCharCode(b);
    }
    return btoa(binary);
  }

  init() {

    // Convert keys from Base64 to ArrayBuffer
    const privateRaw = ChatCrypto.base64ToArrayBuffer(this.myPrivateKeyBase64);
    const publicRaw = ChatCrypto.base64ToArrayBuffer(this.theirPublicKeyBase64);

    // Note: The following lines for parsing publicRaw into x and y coordinates,
    // and assembling uncompressedPoint might be specific to a particular format
    // for representing a "raw" public key. If publicRaw is already in SPKI format,
    // they might not be necessary, as crypto.subtle.importKey("spki", ...)
    // expects a standard structure.
    // const x = publicRaw.slice(0, publicRaw.byteLength / 2);
    // const y = publicRaw.slice(publicRaw.byteLength / 2);
    // const uncompressedPoint = new Uint8Array([0x04, ...new Uint8Array(x), ...new Uint8Array(y)]);

    // Import our private key
    return crypto.subtle.importKey(
      "pkcs8", // Private key format (standard)
      privateRaw,
      { name: "ECDH", namedCurve: "P-256" }, // Algorithm and parameters
      false, // Non-exportable
      ["deriveBits"] // Allowed usage: for deriving bits (shared secret)
    ).then(privateKey =&amp;gt; {
      // Import the contact's public key
      return crypto.subtle.importKey(
        "spki", // Public key format (standard)
        publicRaw,
        { name: "ECDH", namedCurve: "P-256" },
        false, // Non-exportable
        [] // Specific uses are not needed here for the public key in ECDH
      ).then(publicKey =&amp;gt; {
        // 4. Compute the shared secret (deriveBits)
        return crypto.subtle.deriveBits(
          { name: "ECDH", public: publicKey }, // Specify the contact's public key
          privateKey, // Our private key
          256 // Length of the derived secret in bits
        );
      });
    }).then(sharedBits =&amp;gt; {
      // Hash the shared secret to obtain an AES key (using SHA-256 as KDF)
      return crypto.subtle.digest("SHA-256", sharedBits);
    }).then(hashed =&amp;gt; {
      // Import the hashed secret as an AES-GCM key
      return crypto.subtle.importKey(
        "raw", // "Raw" byte format
        hashed, // Hashed secret
        { name: "AES-GCM" }, // Symmetric encryption algorithm
        false, // Non-exportable
        ["encrypt", "decrypt"] // Allowed uses: encryption and decryption
      );
    }).then(aesKey =&amp;gt; {
      this.aesKey = aesKey; // Save the obtained AES key
      return true;         // Signal successful initialization
    });
  }

  encrypt(plaintext) {
    if (!this.aesKey) return Promise.reject("ChatCrypto not initialized");

    // Generate a unique initialization vector (IV)
    const iv = crypto.getRandomValues(new Uint8Array(12)); // 12 bytes (96 bits) is recommended for AES-GCM

    // Convert the text message to bytes (UTF-8)
    const encoded = new TextEncoder().encode(plaintext);

    // Encrypt the data
    return crypto.subtle.encrypt(
      { name: "AES-GCM", iv: iv }, // Algorithm and IV
      this.aesKey, // Our shared AES key
      encoded // Data to encrypt
    ).then(encrypted =&amp;gt; {
      // 4. Return IV and encrypted data (in Base64 for convenient transmission)
      return {
        iv: ChatCrypto.arrayBufferToBase64(iv),
        data: ChatCrypto.arrayBufferToBase64(encrypted)
      };
    });
  }

  decrypt(cipherBase64, ivBase64) {
    if (!this.aesKey) return Promise.reject("ChatCrypto not initialized");

    // Convert ciphertext and IV from Base64 to ArrayBuffer
    const encrypted = ChatCrypto.base64ToArrayBuffer(cipherBase64);
    const ivBuffer = ChatCrypto.base64ToArrayBuffer(ivBase64);

    // Decrypt the data
    return crypto.subtle.decrypt(
      { name: "AES-GCM", iv: new Uint8Array(ivBuffer) }, // Algorithm and IV (must be a TypedArray)
      this.aesKey, // The same shared AES key
      encrypted // Encrypted data
    ).then(decrypted =&amp;gt; {
      // Convert decrypted bytes back to a string
      return new TextDecoder().decode(decrypted);
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;constructor() and base64ToArrayBuffer() method:&lt;br&gt;
The constructor accepts your private key and the contact's public key in Base64 format. Base64 is a way of encoding binary data into a text string, convenient for transmission or storage.&lt;/p&gt;

&lt;p&gt;this.aesKey is initialized as null and will be populated after the init() method successfully completes.&lt;/p&gt;

&lt;p&gt;The static methods base64ToArrayBuffer and arrayBufferToBase64 are used to convert data between Base64 strings and ArrayBuffer (the format Web Crypto API works with).&lt;/p&gt;

&lt;p&gt;init() Method: Establishing the Shared AES Key&lt;br&gt;
This is the heart of our class, where the "magic" of ECDH occurs and the shared key for symmetric encryption is created.&lt;/p&gt;

&lt;p&gt;Breakdown of steps in init():&lt;/p&gt;

&lt;p&gt;Key Conversion: Keys are converted from Base64 to ArrayBuffer.&lt;br&gt;
Import Private Key: Your private key is imported in pkcs8 format. It's specified that this is an ECDH key on the P-256 curve and will be used for deriveBits (computing the shared secret).&lt;/p&gt;

&lt;p&gt;Import Contact's Public Key: The contact's public key is imported in spki format.&lt;/p&gt;

&lt;p&gt;Compute Shared Secret (deriveBits): This is the key ECDH step. Using your private key and the contact's public key, deriveBits computes a shared secret set of bits (sharedBits). This secret will be the same for you and your contact if they use their respective private keys and each other's public keys.&lt;/p&gt;

&lt;p&gt;Hash Shared Secret (digest): sharedBits are hashed using SHA-256. This is a common practice to transform the output of deriveBits into a cryptographically strong key of the desired length for a symmetric cipher (AES in this case). This step also serves as a KDF (Key Derivation Function).&lt;/p&gt;

&lt;p&gt;Import AES Key (importKey): The resulting hash (hashed) is imported as a "raw" key for the AES-GCM algorithm. This key (this.aesKey) is now ready to be used for encrypting and decrypting messages.&lt;br&gt;
After successful execution, this.aesKey will contain a CryptoKey object, ready for use.&lt;/p&gt;

&lt;p&gt;encrypt(plaintext) Method: Encrypting a Message&lt;br&gt;
Breakdown of steps in encrypt():&lt;/p&gt;

&lt;p&gt;Generate IV (Initialization Vector): A random 12-byte IV is created. As a reminder, it must be unique for each encryption with the same key.&lt;/p&gt;

&lt;p&gt;Encode Text: The message is converted from a JavaScript string to a Uint8Array (a sequence of bytes in UTF-8 encoding) using TextEncoder.&lt;br&gt;
Encryption: crypto.subtle.encrypt performs data encryption using AES-GCM, our this.aesKey, and the generated iv.&lt;/p&gt;

&lt;p&gt;Return Result: The encrypted data and IV (both in Base64) are returned as an object. The IV must be transmitted to the recipient along with the ciphertext, as it will be required for decryption.&lt;br&gt;
decrypt(cipherBase64, ivBase64) Method: Decrypting a Message&lt;br&gt;
Breakdown of steps in decrypt():&lt;/p&gt;

&lt;p&gt;Data Conversion: The received ciphertext and IV (in Base64) are converted back to ArrayBuffer. Note that for crypto.subtle.decrypt, the iv parameter must be a TypedArray (e.g., Uint8Array), so we pass new Uint8Array(ivBuffer).&lt;/p&gt;

&lt;p&gt;Decryption: crypto.subtle.decrypt performs the decryption. Importantly, AES-GCM will not only decrypt the data but also verify its integrity and authenticity, using the same aesKey and iv that were used for encryption. If the data has been tampered with, the key is wrong, or the IV is wrong, the decrypt method will return an error (reject the Promise).&lt;/p&gt;

&lt;p&gt;Decode Text: Successfully decrypted bytes are converted back into a readable string using TextDecoder.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How It Works Together: Conceptual Flow
Key Pair Generation:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;User A generates their ECDH key pair (public PkA and private SkA).&lt;br&gt;
User B does the same (public PkB and private SkB). This step is not shown in the provided ChatCrypto code but is shown below in the examples section (it is performed once, for example, during user registration), and it precedes the use of the class. The Web Crypto API has a crypto.subtle.generateKey method for this. The private key Sk must be stored securely and encrypted with the user's password.&lt;br&gt;
Public Key Exchange:&lt;/p&gt;

&lt;p&gt;User A transmits their public key PkA to User B.&lt;br&gt;
User B transmits their public key PkB to User A. This exchange must be secure to avoid Man-in-the-Middle (MitM) attacks. For example, via a secure server or by verifying key fingerprints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ChatCrypto Initialization and Shared Secret Key Computation:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On User A's side: const cryptoA = new ChatCrypto(SkA_base64, PkB_base64); await cryptoA.init();&lt;br&gt;
On User B's side: const cryptoB = new ChatCrypto(SkB_base64, PkA_base64); await cryptoB.init(); As a result, both (cryptoA.aesKey and cryptoB.aesKey) will have computed the same symmetric AES key.&lt;br&gt;
Message Exchange:&lt;/p&gt;

&lt;p&gt;User A encrypts a message for B: const { iv, data } = await chatCryptoA.encrypt("Hello, B!"); Then A sends the { iv, data } object to user B.&lt;/p&gt;

&lt;p&gt;User B receives { iv, data } and decrypts: const message = await chatCryptoB.decrypt(data, iv); // message will be "Hello, B!"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example: Encrypting Text encrypt()&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;// Initialize the class
let chatCrypto = new ChatCrypto( "My_private_key" , "Contact_public_key" );

// Run it
chatCrypto.init().then(() =&amp;gt; {
   chatCrypto.encrypt("Text").then(result =&amp;gt; {

     // The encrypted text will be output
      console.log(result);

  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example: Decrypting Text decrypt()&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;// Initialize the class
let chatCrypto = new ChatCrypto( "My_private_key" , "Contact_public_key" );

// Run it
chatCrypto.init()
  .then( () =&amp;gt; chatCrypto.decrypt("Encrypted_text", "Vector_key_iv") ) // Note: "Vector_key" likely means IV
  .then(result =&amp;gt; {

    // The decrypted text will be output
    console.log(result);

  })
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We've looked at how to implement robust end-to-end message encryption in JavaScript using the Web Crypto API. The combination of ECDH for secure key exchange and AES-GCM for efficient and authenticated data encryption is a powerful and modern approach. The ChatCrypto class serves as a good starting example of such an implementation. Remember the importance of secure generation, storage of private keys, and reliable exchange of public keys to build a secure system.&lt;/p&gt;

</description>
      <category>security</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
