<?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>Forem: Konstantinas Mamonas</title>
    <description>The latest articles on Forem by Konstantinas Mamonas (@konstantinas_mamonas).</description>
    <link>https://forem.com/konstantinas_mamonas</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3067305%2Fa35c9818-12e9-48f4-b339-42e9a0b2a051.jpeg</url>
      <title>Forem: Konstantinas Mamonas</title>
      <link>https://forem.com/konstantinas_mamonas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/konstantinas_mamonas"/>
    <language>en</language>
    <item>
      <title>What’s Inside gzip, zstd, and Other Lossless Compressors</title>
      <dc:creator>Konstantinas Mamonas</dc:creator>
      <pubDate>Mon, 02 Jun 2025 05:00:23 +0000</pubDate>
      <link>https://forem.com/konstantinas_mamonas/whats-inside-gzip-zstd-and-other-lossless-compressors-1fo5</link>
      <guid>https://forem.com/konstantinas_mamonas/whats-inside-gzip-zstd-and-other-lossless-compressors-1fo5</guid>
      <description>&lt;p&gt;Compression shows up everywhere - logs, Kafka, Parquet, file systems, APIs - but most engineers use it without thinking about what's actually happening. You call .compress(), and smaller bytes come out. But what’s under the hood?&lt;/p&gt;

&lt;p&gt;This post is a follow-up to my previous posts &lt;a href="https://dev.to/konstantinas_mamonas/compression-algorithms-you-probably-inherited-gzip-snappy-lz4-zstd-36h0"&gt;Compression Algorithms you probably inherited&lt;/a&gt; and &lt;a href="https://dev.to/konstantinas_mamonas/which-compression-saves-the-most-storage-gzip-snappy-lz4-zstd-1898"&gt;Which Compression Saves the Most Storage $?&lt;/a&gt;. This one focuses on what techniques used in real-world lossless compressors. If you’re choosing a format or just want to understand your tools better, this will help.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Compression Actually Does
&lt;/h2&gt;

&lt;p&gt;Lossless compression shrinks data without sacrificing any of the original information. It achieves this through two primary steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pattern detection&lt;/strong&gt; - identifying and leveraging recurring structures within the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encoding&lt;/strong&gt; - representing these structures using fewer bits than the original representation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;While different compressors employ various combinations of these techniques, the fundamentals remain largely consistent. You can think of it like finding abbreviations for frequently used words to make a text shorter without losing any meaning.&lt;/p&gt;




&lt;h2&gt;
  
  
  Core Techniques
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Run-Length Encoding (RLE)
&lt;/h3&gt;

&lt;p&gt;When a sequence of identical data values occurs consecutively, RLE doesn't store each repetition. Instead, it stores the value once, along with the number of times it repeats.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;:&lt;br&gt;
&lt;code&gt;AAAAABBBB&lt;/code&gt; becomes &lt;code&gt;5A4B&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This technique shines when dealing with data containing long stretches of the same value. Imagine a simple black and white image where many consecutive pixels are the same color, RLE would be very effective here. It's less useful for highly variable or random data where repetitions are rare. In such cases, there are few "runs" to encode.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dictionary Compression (LZ77, LZW)
&lt;/h3&gt;

&lt;p&gt;Instead of repeatedly storing identical sequences of characters or bytes, dictionary compression methods store a single instance of the sequence and then refer back to it whenever it reappears.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LZ77&lt;/strong&gt; maintains a sliding window of recently seen data. When a repeated sequence is found, it's replaced by a pointer indicating the sequence's position and length within the window. Think of it like saying, "the next 3 characters are the same as the 5 characters that appeared 10 positions ago."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LZW&lt;/strong&gt; (Lempel-Ziv-Welch), building upon LZ78, constructs a dictionary of frequently occurring patterns as it processes the data. When a pattern is encountered, it's replaced by its index in the dictionary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These techniques form the bedrock of many popular compression algorithms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DEFLATE&lt;/strong&gt; (used in gzip) combines LZ77 to identify repeated sequences and then uses Huffman coding to efficiently represent the resulting tokens (both literal characters and the length/distance pairs from LZ77).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;zstd&lt;/strong&gt; and &lt;strong&gt;Brotli&lt;/strong&gt; both utilize LZ-style pattern matching as an initial step to reduce redundancy in the data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Entropy Coding
&lt;/h3&gt;

&lt;p&gt;Once the data has been transformed into a more predictable sequence (often the output of pattern matching), entropy coding further reduces its size by assigning shorter bit codes to more frequent symbols. This stage doesn't care about the meaning of the data, only the frequency of the symbols.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Huffman Coding&lt;/strong&gt;: Assigns variable-length bit codes to symbols based on their frequency. More frequent symbols get shorter codes. While fast to decode, it's not perfectly optimal and can sometimes waste up to 1 bit per symbol.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arithmetic Coding&lt;/strong&gt;: Achieves near-optimal compression by representing the entire input sequence as a single fractional number within the range [0, 1). The length of the fraction's binary representation determines the compressed size. It's generally slower than Huffman coding due to the complex calculations involved in manipulating these ranges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ANS (Asymmetric Numeral Systems)&lt;/strong&gt;: A more recent family of entropy coding methods that offer compression ratios close to arithmetic coding but with decoding speeds similar to Huffman coding. It achieves this efficiency by encoding symbols into a single large number in a more streamlined way. zstd and Brotli commonly use variants of ANS, such as FSE (Finite State Entropy), which employs tables for faster processing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The more effectively the data is preprocessed to reveal patterns, the more efficient this final entropy coding stage becomes. For example, if LZ77 has replaced many long repetitions with short pointers, the distribution of these pointers and remaining literal characters will be more skewed towards certain values, allowing Huffman or ANS to assign shorter codes to them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preprocessing Transforms
&lt;/h3&gt;

&lt;p&gt;Sometimes, the inherent structure in data isn't immediately obvious to the compression encoder. Preprocessing transforms reorganize or reformat the data to make these redundancies more apparent.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Burrows–Wheeler Transform (BWT)&lt;/strong&gt;: Rearranges the bytes in a block of data to group identical or similar symbols together. BWT doesn't compress the data on its own, but by creating long runs of identical characters and increasing the frequency of certain symbols, it significantly improves the effectiveness of subsequent RLE and Huffman coding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move-to-Front (MTF)&lt;/strong&gt;: Maintains a list of recently encountered symbols. When a symbol appears, it's moved to the front of the list, and its position in the list (an integer) is outputted. This transform helps entropy coding because frequently occurring symbols will tend to have small position indices, and smaller integers often appear more frequently or have simpler probability distributions, making them easier to compress.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These transforms are often used in pipelines like:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;raw input -&amp;gt; BWT -&amp;gt; MTF -&amp;gt; RLE -&amp;gt; Huffman&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You'll find this specific combination in the bzip2 compressor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chaining Techniques
&lt;/h3&gt;

&lt;p&gt;Real-world compressors don't rely on a single technique. Instead, they strategically combine multiple stages. One stage restructures the data to expose patterns, and the next stage then efficiently encodes those patterns to achieve a smaller size.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Compressor&lt;/th&gt;
&lt;th&gt;Pattern Matching&lt;/th&gt;
&lt;th&gt;Transforms&lt;/th&gt;
&lt;th&gt;Entropy Coding&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gzip&lt;/td&gt;
&lt;td&gt;LZ77&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;Huffman&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zstd&lt;/td&gt;
&lt;td&gt;LZ77-style&lt;/td&gt;
&lt;td&gt;Optional (e.g., prefix)&lt;/td&gt;
&lt;td&gt;FSE (ANS variant)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brotli&lt;/td&gt;
&lt;td&gt;LZ77-style&lt;/td&gt;
&lt;td&gt;MTF, context modeling&lt;/td&gt;
&lt;td&gt;Huffman, ANS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bzip2&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;BWT -&amp;gt; MTF -&amp;gt; RLE&lt;/td&gt;
&lt;td&gt;Huffman&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;gzip is relatively straightforward: it finds repeated substrings using LZ77 and then encodes the resulting stream of literals and (length, distance) pairs with Huffman coding.&lt;/li&gt;
&lt;li&gt;zstd employs fast LZ-style matching and FSE for highly efficient entropy coding, often incorporating optional preprocessing steps to further enhance compression.&lt;/li&gt;
&lt;li&gt;Brotli incorporates transforms like Move-to-Front and sophisticated static/dynamic context models to predict the next symbol, enabling Huffman and ANS to achieve higher compression ratios.&lt;/li&gt;
&lt;li&gt;bzip2 uniquely skips dictionary compression and instead relies on heavy preprocessing (BWT, MTF, RLE) to create highly compressible data for the final Huffman encoding stage.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  A Note on Entropy
&lt;/h2&gt;

&lt;p&gt;In information theory, entropy represents the theoretical minimum number of bits required to represent a unit of data, on average. High-entropy data is characterized by its unpredictability. If there are no discernible patterns, there's little or nothing for a compressor to exploit.&lt;/p&gt;

&lt;p&gt;Random data, such as encrypted information or already highly compressed media, will not compress well and might even increase in size slightly due to the overhead of the compression format's metadata.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Rules of Thumb
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sorted data often compresses better&lt;/strong&gt;&lt;br&gt;
Try sorting logs or other datasets before compression, as this tends to create longer sequences of identical or similar values, improving pattern matching.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Transforms prep the data&lt;/strong&gt;&lt;br&gt;
Techniques like BWT, MTF, and delta encoding don't directly reduce the size of the data. Instead, they reorganize it in a way that makes it more amenable to the subsequent compression stages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compression is workload-dependent&lt;/strong&gt;&lt;br&gt;
Text, logs, and telemetry data typically exhibit significant redundancy and compress well. In contrast, encrypted data lacks patterns, and data with very high cardinality (many unique values) offers fewer opportunities for compression.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why It Matters
&lt;/h2&gt;

&lt;p&gt;Compression's impact extends far beyond just saving disk space. It influences aspects of your data pipelines, including Kafka throughput, Parquet read latency, network transfer times, and CPU utilization. An understanding of how compressors work helps to make informed decisions about which algorithms and settings to use, ultimately leading to better performance and resource efficiency.&lt;/p&gt;




&lt;p&gt;Thank you for reading.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>performance</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Parquet? What Parquet?</title>
      <dc:creator>Konstantinas Mamonas</dc:creator>
      <pubDate>Mon, 26 May 2025 07:02:38 +0000</pubDate>
      <link>https://forem.com/konstantinas_mamonas/parquet-what-parquet-5hfc</link>
      <guid>https://forem.com/konstantinas_mamonas/parquet-what-parquet-5hfc</guid>
      <description>&lt;p&gt;If you’re in data, you’re probably using Parquet. It’s not officially the standard, but good luck trying to convince anyone to use something else.&lt;/p&gt;

&lt;p&gt;This post is meant to open the black box that is Parquet to see what exactly makes it so damn good. I’ll give a breakdown of the internals and show you some optimizations on how to go from a badly optimized file to a &lt;em&gt;blazingly fast™&lt;/em&gt; one.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Even &lt;em&gt;Is&lt;/em&gt; Parquet?
&lt;/h2&gt;

&lt;p&gt;Parquet is a &lt;strong&gt;columnar storage format&lt;/strong&gt;, it stores data &lt;strong&gt;by column&lt;/strong&gt;, not by row. That’s ideal for analytical queries like &lt;code&gt;SUM(driver_pay)&lt;/code&gt; or &lt;code&gt;WHERE trip_miles &amp;lt; 2&lt;/code&gt;, where you only need a few columns, not the entire row. Engines can skip the rest, making reads faster and more efficient.&lt;/p&gt;

&lt;p&gt;A Parquet file is composed of several layers, each designed to improve performance and storage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Columnar layout&lt;/strong&gt;: Values from the same column are stored together, enabling tight compression and efficient scans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row groups&lt;/strong&gt;: Horizontal partitions of the dataset. These are the unit of parallelism and skipping; each contains all columns for a batch of rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Column chunks&lt;/strong&gt;: Within each row group, data is organized by column.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pages&lt;/strong&gt;: Each column chunk is divided into pages (typically ~8KB). These are the unit of encoding and compression, allowing for localized reads and decompression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encodings&lt;/strong&gt;: Within pages, data can be encoded (dictionary, run-length, bit-packing) to reduce size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compression&lt;/strong&gt;: Pages can be compressed (ZSTD, Snappy, etc.) for storage and I/O efficiency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata&lt;/strong&gt;: stores stats and min/max indexes so engines can skip row groups that don’t match the filter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This layered structure makes Parquet efficient, but also means you have a lot of tuning knobs and plenty of ways to screw things up if you’re not careful.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Tested
&lt;/h2&gt;

&lt;p&gt;The original data comes from NYC’s TLC trip records, merged into a 1.6GB uncompressed Parquet file. One of the files I used in &lt;a href="https://dev.to/konstantinas_mamonas/which-compression-saves-the-most-storage-gzip-snappy-lz4-zstd-1898"&gt;Which Compression Saves the Most Storage $? (gzip, Snappy, LZ4, zstd)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I used PyArrow to generate multiple Parquet variants, each applying a small tweak, like new row group sizes, better encoding, compression and sorting to isolate the effect.&lt;/p&gt;

&lt;p&gt;Then I ran DuckDB queries to benchmark performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MacBook Pro (2021, M1 Pro, 16GB RAM)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Variant Overview
&lt;/h2&gt;

&lt;p&gt;Here’s a quick snapshot of what changed in each file.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;RGs&lt;/th&gt;
&lt;th&gt;Comp&lt;/th&gt;
&lt;th&gt;Enc&lt;/th&gt;
&lt;th&gt;Dict&lt;/th&gt;
&lt;th&gt;Sorted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;00_Worst&lt;/td&gt;
&lt;td&gt;7.40 GB&lt;/td&gt;
&lt;td&gt;60,304&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;V1&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;01_Base&lt;/td&gt;
&lt;td&gt;6.83 GB&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;V1&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02a_Dict&lt;/td&gt;
&lt;td&gt;1.67 GB&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;V1&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02b_Comp&lt;/td&gt;
&lt;td&gt;1.82 GB&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;ZSTD&lt;/td&gt;
&lt;td&gt;V1&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03_Dict+Comp&lt;/td&gt;
&lt;td&gt;1.37 GB&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;ZSTD&lt;/td&gt;
&lt;td&gt;V1&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;04a_OptNS&lt;/td&gt;
&lt;td&gt;1.38 GB&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;ZSTD&lt;/td&gt;
&lt;td&gt;V2&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;04b_SortV1&lt;/td&gt;
&lt;td&gt;1.88 GB&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;ZSTD&lt;/td&gt;
&lt;td&gt;V1&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05b_OptSV2&lt;/td&gt;
&lt;td&gt;1.88 GB&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;ZSTD&lt;/td&gt;
&lt;td&gt;V2&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Legend&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Variant&lt;/strong&gt;: Short ID for each Parquet file variant
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size&lt;/strong&gt;: Final file size on disk
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RGs&lt;/strong&gt;: Number of row groups
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comp&lt;/strong&gt;: Compression codec used (&lt;code&gt;ZSTD&lt;/code&gt; or &lt;code&gt;None&lt;/code&gt;)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enc&lt;/strong&gt;: Parquet data page version (&lt;code&gt;V1&lt;/code&gt; or &lt;code&gt;V2&lt;/code&gt;)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dict&lt;/strong&gt;: Whether dictionary encoding was enabled
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sorted&lt;/strong&gt;: Whether the file was sorted by &lt;code&gt;PULocationID&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How come the worst file ballooned to 7.40GB?
&lt;/h2&gt;

&lt;p&gt;In the post about compression, the same uncompressed file was 1.6GB. What happened here?&lt;/p&gt;

&lt;p&gt;I thought, damn, I must have uncompressed it wrong last time making the previous post invalid, however I wasn't wrong.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;02a_Dict&lt;/code&gt; file confirms that it's dictionary encoding that significantly reduced the size. That alone caused the file to bloat from 1.6GB to 7.4GB. With it on, the size drops back down even without compression. Interestingly, compression alone &lt;code&gt;02b_Comp&lt;/code&gt; doesn't push the file size lower than just dictionary encoding.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the Internals Affected Query Performance
&lt;/h2&gt;

&lt;p&gt;I ran seven queries on each file variant: filters, projections, aggregations, and full row reads.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BENCHMARK_QUERIES = {
    "1. Count with selective filter (predicate pushdown on numeric 'trip_miles')": "SELECT COUNT(*) FROM read_parquet('{file}') WHERE trip_miles &amp;lt; 2.0;",
    "2. Projection of specific columns (column pruning 'pickup_datetime', 'dropoff_datetime')": "SELECT pickup_datetime, dropoff_datetime FROM read_parquet('{file}') LIMIT 1000;",
    "3. Aggregation on filtered data (mixed types, computation)": "SELECT AVG(trip_miles), SUM(base_passenger_fare) FROM read_parquet('{file}') WHERE tips &amp;gt; 0.0 AND trip_time &amp;gt; 600;",
    "4. Filter on low-cardinality string (dictionary encoding potential 'hvfhs_license_num')": "SELECT COUNT(*) FROM read_parquet('{file}') WHERE hvfhs_license_num = 'HV0003';",
    "5. Full scan and sum of one numeric column (I/O and decompression 'driver_pay')": "SELECT SUM(driver_pay) FROM read_parquet('{file}');",
    "6. Count with filter on an integer ID (predicate pushdown 'PULocationID')": "SELECT COUNT(*) FROM read_parquet('{file}') WHERE PULocationID = 148;",
    "7. Full row reconstruction (worst-case for column store, limited)": "SELECT * FROM read_parquet('{file}') LIMIT 10;",
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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




&lt;h3&gt;
  
  
  Row Group Size Changed Everything
&lt;/h3&gt;

&lt;p&gt;Just fixing the row group size and going from 60K tiny chunks to 60 made the biggest difference in performance.&lt;/p&gt;

&lt;p&gt;In the baseline, a simple &lt;code&gt;COUNT(*) WHERE trip_miles &amp;lt; 2&lt;/code&gt; took 3.662s. With improved row groups? 0.124s. Same data, same engine, 28x faster.&lt;/p&gt;

&lt;p&gt;Small row groups mean a ton of metadata scanning and disk seeks. Fewer, larger row groups = more I/O locality and less overhead.&lt;/p&gt;

&lt;h4&gt;
  
  
  Impact of Larger Row Groups (4.37M vs. 1M Rows per RG)
&lt;/h4&gt;

&lt;p&gt;Official Parquet documentation recommends large row groups &lt;strong&gt;512MB - 1GB&lt;/strong&gt; for uncompressed data. In our case this would have been around 14 row groups, however after trying that in practice I noticed something interesting, keeping all things the same, the file size has increased. This seems to be caused by dictionary encoding as within larger row groups there might be more unique values within the column, which results in larger dictionaries.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Rows per RG&lt;/th&gt;
&lt;th&gt;Row Groups&lt;/th&gt;
&lt;th&gt;File Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;04a_OptNS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~1M (1,018,964)&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;1375.28 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;04a_OptNS_LargeRG&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~4.37M (4,370,585)&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;1444.29 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What about performance?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query Type&lt;/th&gt;
&lt;th&gt;&lt;code&gt;04a_OptNS&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;04a_OptNS_LargeRG&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;% Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Count with selective filter (numeric)&lt;/td&gt;
&lt;td&gt;0.1633&lt;/td&gt;
&lt;td&gt;0.1664&lt;/td&gt;
&lt;td&gt;+1.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Projection of specific columns&lt;/td&gt;
&lt;td&gt;0.0055&lt;/td&gt;
&lt;td&gt;0.0070&lt;/td&gt;
&lt;td&gt;+27.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Aggregation on filtered data&lt;/td&gt;
&lt;td&gt;0.3541&lt;/td&gt;
&lt;td&gt;0.3680&lt;/td&gt;
&lt;td&gt;+3.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Filter on low-cardinality string&lt;/td&gt;
&lt;td&gt;0.0934&lt;/td&gt;
&lt;td&gt;0.1018&lt;/td&gt;
&lt;td&gt;+9.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Full scan and sum of one numeric column&lt;/td&gt;
&lt;td&gt;0.0883&lt;/td&gt;
&lt;td&gt;0.0918&lt;/td&gt;
&lt;td&gt;+4.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Count with filter on an integer ID&lt;/td&gt;
&lt;td&gt;0.0718&lt;/td&gt;
&lt;td&gt;0.0788&lt;/td&gt;
&lt;td&gt;+9.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7. Full row reconstruction (worst-case for column store)&lt;/td&gt;
&lt;td&gt;0.0298&lt;/td&gt;
&lt;td&gt;0.0302&lt;/td&gt;
&lt;td&gt;+1.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In my opinion the cause of the slower performance was mostly due to CPU having to do more work to decompress that larger block in one go.&lt;/p&gt;

&lt;p&gt;This highlights a critical point: while a general recommendation for row group size exists, the optimal size is often workload and data dependent. In my case I kept Row Groups at 1M.&lt;/p&gt;




&lt;h3&gt;
  
  
  Compression Was Worth It
&lt;/h3&gt;

&lt;p&gt;ZSTD reduced the file size from 6.83 GB to 1.82 GB and kept query times low, often under 0.2s for filtered counts and scans. It adds minimal CPU overhead compared to the I/O savings.&lt;/p&gt;

&lt;p&gt;As such, for full scans like &lt;code&gt;SUM(driver_pay)&lt;/code&gt; neither compression alone nor with dictionary encoding outperform the &lt;code&gt;01_Base&lt;/code&gt; version.&lt;/p&gt;

&lt;p&gt;So, the overhead of compression and dictionary encoding slows it down a bit. However the results make sense as compression isn't meant to improve your query execution speeds but reduce the size of the data.&lt;/p&gt;




&lt;h3&gt;
  
  
  Encodings and Page Versions Gave Smaller Gains
&lt;/h3&gt;

&lt;p&gt;Switching to better encodings made small improvements in file size. If you apply them without compression, they can be extremely powerful on their own, but combined they will truly shine.&lt;/p&gt;

&lt;p&gt;The column &lt;code&gt;hvfhs_license_num&lt;/code&gt; has low cardinality, just a few distinct values. Fixing the tiny row groups improved that to &lt;strong&gt;0.085s&lt;/strong&gt;. Turning on dictionary encoding &lt;code&gt;02a_Dict&lt;/code&gt; brought a small additional gain: &lt;strong&gt;0.0846s&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Since in most cases encoding reduced performance due to additional overhead for space savings, I will take a W.&lt;/p&gt;

&lt;p&gt;Parquet V2 pages gave marginal improvements. They’re worth turning on if your engine supports them, but they won’t move the needle like row groups or compression will.&lt;/p&gt;




&lt;h3&gt;
  
  
  Sorting Provided Targeted Speedups
&lt;/h3&gt;

&lt;p&gt;Sorting the file by &lt;code&gt;PULocationID&lt;/code&gt; had a huge impact on one query: a count filter on that column.&lt;/p&gt;

&lt;p&gt;Unsorted: &lt;strong&gt;0.07s&lt;/strong&gt;&lt;br&gt;
Sorted: &lt;strong&gt;0.007s&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's a 10x improvement from better row group skipping. The rest of the queries didn’t change much, and the file got a bit larger, but it’s a trade-off that’s worth it if you know your access patterns.&lt;/p&gt;




&lt;h3&gt;
  
  
  Even Worst-Case Row Reads Got Faster
&lt;/h3&gt;

&lt;p&gt;Reconstructing full rows (all columns, LIMIT 10) is the worst thing you can do to a columnar format.&lt;/p&gt;

&lt;p&gt;Worst: &lt;strong&gt;2.27s&lt;/strong&gt;&lt;br&gt;
Optimized: &lt;strong&gt;0.023s&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Good structure helps even when Parquet’s working against you.&lt;/p&gt;




&lt;h3&gt;
  
  
  So what about it?
&lt;/h3&gt;

&lt;p&gt;Parquet has sensible defaults, and if you’re not actively trying to sabotage them, things usually work fine. But if you know which knobs to turn, you can make it &lt;em&gt;blazingly fast™&lt;/em&gt;. Kachow.&lt;/p&gt;

&lt;p&gt;The takeaway - the defaults don’t know your workload. You do.&lt;/p&gt;




&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>performance</category>
      <category>python</category>
      <category>programming</category>
      <category>learning</category>
    </item>
    <item>
      <title>Which Compression Saves the Most Storage $? (gzip, Snappy, LZ4, zstd)</title>
      <dc:creator>Konstantinas Mamonas</dc:creator>
      <pubDate>Mon, 19 May 2025 07:07:32 +0000</pubDate>
      <link>https://forem.com/konstantinas_mamonas/which-compression-saves-the-most-storage-gzip-snappy-lz4-zstd-1898</link>
      <guid>https://forem.com/konstantinas_mamonas/which-compression-saves-the-most-storage-gzip-snappy-lz4-zstd-1898</guid>
      <description>&lt;p&gt;Compression setting are set and forget in most cases, if it works no reason to change it. I decided to look into and see whether it would be beneficial to review the defaults and if it could save money. I covered most of the algorithms discussed in this post previously in &lt;a href="https://dev.to/konstantinas_mamonas/compression-algorithms-you-probably-inherited-gzip-snappy-lz4-zstd-36h0"&gt;Compression Algorithms You Probably Inherited&lt;/a&gt;, where I summarized the info I collected while researching. But I wanted to sanity-check the findings myself and decided to run some benchmarks. This should help me see if compression actually makes a difference for storage costs.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Tested
&lt;/h2&gt;

&lt;p&gt;To keep things real, I used actual data: &lt;a href="https://www.nyc.gov/site/tlc/about/tlc-trip-record-data.page" rel="noopener noreferrer"&gt;NYC TLC trip records&lt;/a&gt;. Each month’s data file was ~500MB. I combined a few to get files at 500MB, 1.6GB, 3.9GB, and 6.6GB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compression algorithms tested:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;gzip&lt;/li&gt;
&lt;li&gt;Snappy&lt;/li&gt;
&lt;li&gt;LZ4&lt;/li&gt;
&lt;li&gt;zstd at levels 1, 3, 9, and 19&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Environment:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MacBook Pro (2021, M1 Pro, 16GB RAM)&lt;/li&gt;
&lt;li&gt;Single-threaded runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I couldn’t process the largest file with my setup. SKILL ISSUE. In reality, I didn’t bother trying to fix it, multi-threading and batching the compression should have allowed me to do it, but I already had the 3 other files to work with.&lt;/p&gt;

&lt;p&gt;To run the benchmarks, I built a small CLI tool: &lt;a href="https://github.com/KonMam/compressbench" rel="noopener noreferrer"&gt;compressbench&lt;/a&gt;. It’s publicly available and it currently supports gzip, snappy, lz4, and zstd (with levels) and outputs compression/decompression benchmarks for Parquet files. I’m planning to add support for custom codecs later, mostly so I can benchmark my own RLE, Huffman, and LZ77 implementations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Results
&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%2F5lorxlebnjx4e3npqyyv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5lorxlebnjx4e3npqyyv.png" alt="Compression Ratio/Time/Throughput, Decompression Throughput" width="800" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Compression Ratio
&lt;/h3&gt;

&lt;p&gt;If you only care about the smallest file, zstd-19 and gzip come out ahead. But the margin over zstd-3 is tiny, and you pay for it heavily elsewhere. Snappy and LZ4 compress to about 1.12 - just enough to make it look like they tried. But if that’s all you have, 12% savings is still better than no savings.&lt;/p&gt;

&lt;p&gt;For most storage use cases, zstd-3 gets close enough to the “best” ratio without turning your CPU into a space heater.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compression Speed
&lt;/h3&gt;

&lt;p&gt;Snappy and LZ4 are fast. zstd-1, 3, and 9 kept up surprisingly well. gzip is predictably slow. zstd-19 made me question my life choices, I thought it might have frozen or got silently murdered by the OS. I’m not saying never use it, there are some use cases, but they’re likely few and far between.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decompression Speed
&lt;/h3&gt;

&lt;p&gt;Snappy and LZ4 hit over 3.5GB/s. zstd held steady around 1GB/s across all levels. gzip stayed slow.&lt;/p&gt;

&lt;p&gt;If you need to read the same data multiple times Snappy and LZ4 are faster than gzip or zstd. But zstd isn’t slow enough to matter unless your volumes are huge.&lt;/p&gt;




&lt;h3&gt;
  
  
  File Size Scaling
&lt;/h3&gt;

&lt;p&gt;Throughput went down as file size grew. Gzip was slow the whole time. zstd-19 was even slower and I didn't run it for all file size, so it may have gone even more down.&lt;/p&gt;

&lt;p&gt;The others held up fairly well. Snappy stayed fastest, but none of them completely fell apart.&lt;/p&gt;

&lt;p&gt;Note: CPU was pinned at 100% during all runs. On a single-threaded, 16GB machine, there was probably some memory pressure too for the larger files. These results match what I’ve seen elsewhere but might be a bit exaggerated.&lt;/p&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%2Fancdw6yt0v9ipuxiw5mi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fancdw6yt0v9ipuxiw5mi.png" alt="Compression Throughput vs File Size" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Storage Cost (S3)
&lt;/h3&gt;

&lt;p&gt;S3 Standard pricing in eu-central-1 is $0.0235/GB. At 500TB/month, codec choice can have a significant (based on your budged) impact on the cost. But if you're only storing a few TB, this doesn’t matter much. At 100TB, you're looking at maybe a few hundred bucks.&lt;/p&gt;

&lt;p&gt;Snappy/LZ4 would cost around $10.7K/month. zstd-3 lands near $9.7K for 500TBs. zstd-19 saves a bit more, but the compute cost and latency make it hard to justify. gzip is in the same ballpark, and we’ve already covered its performance.&lt;/p&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%2Fkf9gvdnlb94ydp6sd3xa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkf9gvdnlb94ydp6sd3xa.png" alt="Projected Monthly S3 Cost by Codec at Scale" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What to Pick?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For streaming&lt;/strong&gt;&lt;br&gt;
Snappy or LZ4. Fast compression and decompression. Compression ratio better than nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For batch ETL or periodic jobs&lt;/strong&gt;&lt;br&gt;
zstd-1 or zstd-3. Good balance between speed and size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For archival&lt;/strong&gt;&lt;br&gt;
zstd-9 if you care for small gains. zstd-19 if you’re archiving something you hope nobody ever reads again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;After my initial post, I assumed the real-life impact of LZ4 and zstd would be more obvious. But it turns out you need quite a bit of scale to feel it. In the future, I won’t be so quick to dismiss Snappy as it has its place. But it’s not the only viable option there is.&lt;/p&gt;

&lt;p&gt;I'd also like to benchmark compute cost in the future and see whether using zstd at scale is actually worth it for batch processes or if the additional compute time eats up your storage savings.&lt;/p&gt;

&lt;p&gt;Also keep in mind that your mileage might vary based on your data, compression is about finding patters and if there's none, the result might be a larger file than you began with, so pick accordingly, maybe run a benchmark yourself.&lt;/p&gt;




&lt;p&gt;Thank you for reading.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>performance</category>
      <category>python</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Compression Algorithms You Probably Inherited: gzip, Snappy, LZ4, zstd</title>
      <dc:creator>Konstantinas Mamonas</dc:creator>
      <pubDate>Mon, 12 May 2025 07:31:25 +0000</pubDate>
      <link>https://forem.com/konstantinas_mamonas/compression-algorithms-you-probably-inherited-gzip-snappy-lz4-zstd-36h0</link>
      <guid>https://forem.com/konstantinas_mamonas/compression-algorithms-you-probably-inherited-gzip-snappy-lz4-zstd-36h0</guid>
      <description>&lt;h2&gt;
  
  
  You Might Be Using The Wrong Compression Algorithm
&lt;/h2&gt;

&lt;p&gt;If you work in data engineering, you’ve probably used &lt;strong&gt;gzip&lt;/strong&gt;, &lt;strong&gt;Snappy&lt;/strong&gt;, &lt;strong&gt;LZ4&lt;/strong&gt;, or &lt;strong&gt;Zstandard (zstd)&lt;/strong&gt;. More likely - you inherited them. Either the person who set these defaults is long gone, there’s never enough time to revisit the choice, or things work well enough and you’d rather not duck around and find out otherwise.&lt;/p&gt;

&lt;p&gt;Most engineers stick with the defaults. Changing them feels risky. And let’s be honest - many don’t really know what these algorithms do or why one was chosen in the first place.&lt;/p&gt;

&lt;p&gt;I’ve been that person myself: &lt;em&gt;"Oh, we’re using Snappy? OK."&lt;/em&gt; Never thinking to ask why or what else we could use.&lt;/p&gt;

&lt;p&gt;This post explains the most common compression algorithms, what makes them different, and when you should actually use each.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Compression Choices Matter
&lt;/h2&gt;

&lt;p&gt;Compression decisions aren’t just about saving space. They directly impact:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Storage costs&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU utilization&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Throughput&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Latency&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In modern pipelines — Kafka, Parquet, column stores, data lakes - the wrong compression algorithm can degrade all of these.&lt;/p&gt;

&lt;p&gt;Two metrics matter most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compression ratio&lt;/strong&gt;: How much smaller the data gets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throughput&lt;/strong&gt;: How quickly data can be compressed and decompressed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your workload - and whether you prioritize CPU, latency, or bandwidth - determines which trade-offs are acceptable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Main Culprits
&lt;/h2&gt;

&lt;h3&gt;
  
  
  gzip
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What it is&lt;/strong&gt;: Uses the DEFLATE algorithm (LZ77 + Huffman coding).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Goal&lt;/strong&gt;: Good compression ratio. Compatibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt;: Slow to compress. Moderate decompression speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strength&lt;/strong&gt;: Ubiquitous. Supported everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weakness&lt;/strong&gt;: Outclassed in both speed and compression ratio by newer algorithms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use&lt;/strong&gt;: Archival, compatibility with legacy tools. Otherwise, avoid.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Snappy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What it is&lt;/strong&gt;: Developed by Google. Based on LZ77 without entropy coding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Goal&lt;/strong&gt;: Maximize speed, not compression ratio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt;: Very fast compression and decompression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strength&lt;/strong&gt;: Low CPU overhead. Stable. Production-proven at Google scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weakness&lt;/strong&gt;: Larger compressed size than other options.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use&lt;/strong&gt;: Real-time, low-CPU systems where latency matters more than storage. Or if you're stuck with it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  LZ4
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What it is&lt;/strong&gt;: LZ77-based. Prioritizes speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Goal&lt;/strong&gt;: Fast compression and decompression with moderate compression ratio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt;: &amp;gt; 500 MB/s compression. GB/s decompression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strength&lt;/strong&gt;: Extremely fast. Low CPU usage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weakness&lt;/strong&gt;: Compression ratio lower than gzip or zstd.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use&lt;/strong&gt;: High-throughput, low-latency systems. Datacenter transfers. OLAP engines (DuckDB, Cassandra).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  zstd (Zstandard)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What it is&lt;/strong&gt;: Developed by Facebook. Combines LZ77, Huffman coding, and FSE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Goal&lt;/strong&gt;: High compression ratio with fast speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt;: Compression 500+ MB/s. Decompression ~1500+ MB/s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strength&lt;/strong&gt;: Tunable. Balances speed and compression. Strong performance across data types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weakness&lt;/strong&gt;: Slightly more CPU than LZ4/Snappy at default settings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When to use&lt;/strong&gt;: General-purpose. Parquet files. Kafka. Data transfers. Usually the best all-around choice.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Strengths and Weaknesses (At a Glance)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Algorithm&lt;/th&gt;
&lt;th&gt;Compression Ratio&lt;/th&gt;
&lt;th&gt;Compression Speed&lt;/th&gt;
&lt;th&gt;Decompression Speed&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gzip&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Slow&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Archival, web content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snappy&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Very Fast&lt;/td&gt;
&lt;td&gt;Very Fast&lt;/td&gt;
&lt;td&gt;Real-time, low-CPU systems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LZ4&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Extremely Fast&lt;/td&gt;
&lt;td&gt;Extremely Fast&lt;/td&gt;
&lt;td&gt;High-throughput, low-latency systems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zstd&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;General-purpose, Parquet, Kafka, data transfers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Real-World Scenarios: When to Use What
&lt;/h2&gt;

&lt;h3&gt;
  
  
  High-throughput streaming (Kafka)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use&lt;/strong&gt;: zstd or LZ4&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt;: zstd gives better compression with good speed. LZ4 if latency is critical and CPU is limited. Snappy is acceptable if inherited, but usually not optimal anymore.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Long-term storage (Parquet, S3)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use&lt;/strong&gt;: zstd&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt;: Best compression ratio reduces storage cost and IO. Slight CPU trade-off is acceptable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Low-latency querying (DuckDB, Cassandra)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use&lt;/strong&gt;: LZ4&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt;: Prioritize decompression speed for fast queries. LZ4 is the common choice in OLAP engines.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  CPU/memory constrained environments
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use&lt;/strong&gt;: Snappy or LZ4&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt;: Low CPU overhead is more important than compression ratio. zstd can still be used at low compression levels if needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fast network, low compression benefit (datacenter file transfer)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use&lt;/strong&gt;: LZ4&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt;: Minimal compression overhead. On fast networks, speed beats smaller file sizes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Slow network or internet transfers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use&lt;/strong&gt;: zstd&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt;: Better compression reduces transfer time despite slightly higher CPU cost.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What to Remember
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No algorithm is best for every workload.&lt;/li&gt;
&lt;li&gt;zstd has become the Swiss Army knife of compression. Unless you have a good reason not to, it’s a smart pick.&lt;/li&gt;
&lt;li&gt;LZ4 is unbeatable when speed matters more than compression.&lt;/li&gt;
&lt;li&gt;Snappy is still acceptable in latency-sensitive, CPU-constrained setups but is generally being replaced.&lt;/li&gt;
&lt;li&gt;gzip remains for legacy systems or when maximum compatibility is required.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Underneath The Hood
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;LZ77&lt;/strong&gt; - Replaces repeated sequences of data with references to earlier copies in the stream (sliding window). &lt;a href="https://en.wikipedia.org/wiki/LZ77_and_LZ78" rel="noopener noreferrer"&gt;Wikipedia&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Huffman Coding&lt;/strong&gt; - A method of assigning shorter codes to more frequent data patterns to save space. &lt;a href="https://en.wikipedia.org/wiki/Huffman_coding" rel="noopener noreferrer"&gt;Wikipedia&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;FSE (Finite State Entropy)&lt;/strong&gt; - An advanced entropy coding method that efficiently compresses sequences by balancing speed and compression ratio. &lt;a href="https://facebook.github.io/zstd/" rel="noopener noreferrer"&gt;Facebook’s zstd Manual&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why it matters&lt;/strong&gt;&lt;br&gt;
Most compression algorithms combine finding patterns (LZ77) with efficient encoding (Huffman, FSE) to shrink data without losing information.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;Compression choices tend to stick around. There’s rarely time to revisit legacy pipelines, and if something works, it’s easy to assume it’s good enough. But if you can make the time, you’re now better equipped to review your defaults (I know I am.) - and see if a different choice might better fit your needs.&lt;/p&gt;




&lt;p&gt;Thank you for reading.&lt;/p&gt;

</description>
      <category>database</category>
      <category>sql</category>
      <category>programming</category>
      <category>performance</category>
    </item>
    <item>
      <title>DuckDB: When You Don’t Need Spark (But Still Need SQL)</title>
      <dc:creator>Konstantinas Mamonas</dc:creator>
      <pubDate>Sat, 03 May 2025 13:48:38 +0000</pubDate>
      <link>https://forem.com/konstantinas_mamonas/duckdb-when-you-dont-need-spark-but-still-need-sql-1p9c</link>
      <guid>https://forem.com/konstantinas_mamonas/duckdb-when-you-dont-need-spark-but-still-need-sql-1p9c</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Too often, data engineering tasks that should be simple end up requiring heavyweight tools. Something breaks, or I need to explore a new dataset, and suddenly I’m firing up Spark or connecting to a cloud warehouse - even though the data easily fits on my laptop. That adds extra steps, slows things down, and costs more than it should. I wanted something simpler for local analytics that could still handle serious queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is DuckDB?
&lt;/h2&gt;

&lt;p&gt;DuckDB is an open-source, in-process SQL OLAP database designed for analytics.&lt;/p&gt;

&lt;p&gt;It runs embedded inside applications, similar to SQLite, but optimized for analytical queries like joins, aggregations, and large scans.&lt;/p&gt;

&lt;p&gt;In short, it goes fast without adding the complexity of distributed systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  How DuckDB Achieves High Performance
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Columnar Storage:&lt;/strong&gt;&lt;br&gt;
Data is stored by columns, not rows. This lets queries scan only the data they need, cutting down IO.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vectorized Execution:&lt;/strong&gt;&lt;br&gt;
Processes data in batches (about 1000 rows at a time) to leverage CPU caching and SIMD instructions, reducing processing overhead.&lt;/p&gt;

&lt;p&gt;These two design choices allow DuckDB to handle complex analytical queries efficiently on a single machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Large Datasets
&lt;/h2&gt;

&lt;p&gt;DuckDB dynamically manages memory and disk usage based on workload size:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In-Memory Mode:&lt;/strong&gt; Keeps everything in RAM if possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Out-of-Core Mode:&lt;/strong&gt; Spills to disk if data exceeds memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid Execution:&lt;/strong&gt; Switches between modes automatically based on workload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent Storage:&lt;/strong&gt; Can save results in &lt;code&gt;.duckdb&lt;/code&gt; files for reuse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No manual configuration. No crashing on out-of-memory errors (Hi Pandas!).&lt;/p&gt;

&lt;h2&gt;
  
  
  Extensibility &amp;amp; Concurrency
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Single-writer, multiple-reader concurrency (MVCC).&lt;/li&gt;
&lt;li&gt;Growing ecosystem of extensions: Parquet, CSV, S3, HTTP endpoints, geospatial analytics.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Trade-Offs: DuckDB vs Specialized Engines
&lt;/h2&gt;

&lt;p&gt;DuckDB is flexible and fast, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQL Parsing Overhead:&lt;/strong&gt; Engines like Polars can be faster for simple dataframe operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;General Purpose Design:&lt;/strong&gt; Flexibility trades off some raw speed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That said, for most data engineering tasks, the trade-off is worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where DuckDB Shines
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Local dataset exploration (when Pandas hits limits).&lt;/li&gt;
&lt;li&gt;CI and pipeline testing without Spark.&lt;/li&gt;
&lt;li&gt;Batch transformations on Parquet, CSV, and other formats.&lt;/li&gt;
&lt;li&gt;Lightweight production workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits to Keep in Mind
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Single-machine only - limited by your hardware.&lt;/li&gt;
&lt;li&gt;Not built for transactional workloads.&lt;/li&gt;
&lt;li&gt;SQL pipelines can get messy if not managed well.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reflection: Why This Matters
&lt;/h2&gt;

&lt;p&gt;DuckDB helps bridge the gap between dataset size and engineering overhead.It’s not about replacing big tools, but avoiding them when you don’t need them.&lt;/p&gt;

&lt;p&gt;For tasks that outgrow Pandas or require complex queries, it’s a practical alternative to heavier tools.&lt;/p&gt;




&lt;p&gt;Thanks for reading.&lt;/p&gt;

</description>
      <category>database</category>
      <category>sql</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>kafka-replay-cli: A Lightweight Kafka Replay &amp; Debugging Tool</title>
      <dc:creator>Konstantinas Mamonas</dc:creator>
      <pubDate>Sat, 03 May 2025 07:12:22 +0000</pubDate>
      <link>https://forem.com/konstantinas_mamonas/kafka-replay-cli-a-lightweight-kafka-replay-debugging-tool-19h5</link>
      <guid>https://forem.com/konstantinas_mamonas/kafka-replay-cli-a-lightweight-kafka-replay-debugging-tool-19h5</guid>
      <description>&lt;h2&gt;
  
  
  Project Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/KonMam/kafka-replay-cli" rel="noopener noreferrer"&gt;github.com/KonMam/kafka-replay-cli&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PyPI&lt;/strong&gt;: &lt;a href="https://pypi.org/project/kafka-replay-cli/" rel="noopener noreferrer"&gt;pypi.org/project/kafka-replay-cli&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I Built This
&lt;/h2&gt;

&lt;p&gt;I wanted more hands-on &lt;a href="https://kafka.apache.org/" rel="noopener noreferrer"&gt;Kafka&lt;/a&gt; experience - that's the gist of it. Before this, I’d dealt with a few producers/consumers here and there, read the docs, and studied Kafka’s architectural design principles (very insightful read if you are interested in that sort of thing: &lt;a href="https://kafka.apache.org/documentation/" rel="noopener noreferrer"&gt;https://kafka.apache.org/documentation/&lt;/a&gt;).&lt;br&gt;
But there’s only so much you can learn with limited exposure and just reading, so I decided to spend some time tinkering and learning by doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Goals
&lt;/h2&gt;

&lt;p&gt;There were a few things I wanted to achieve with this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Get more Kafka experience - main goal.&lt;/li&gt;
&lt;li&gt;Integrate &lt;a href="https://duckdb.org/" rel="noopener noreferrer"&gt;DuckDB&lt;/a&gt; - for the past year, I have seen a lot of hype around it and have started using it for some ad-hoc analysis. I enjoy using it, so I wanted to find a place for it.&lt;/li&gt;
&lt;li&gt;Have something to show at the end of it - meaning, find a real issue that people using Kafka might have and develop something around it, applying good practices.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Problem &amp;amp; MVP
&lt;/h2&gt;

&lt;p&gt;I needed to find a problem I could so-called 'solve,' even if it had been done before. After some careful Googling and ChatGPT-ing, &lt;strong&gt;Kafka message replay&lt;/strong&gt; came up as something people either struggle with or need heavy tools to handle. The tool should be useful for someone who needs to reprocess events with filters or transformations, debugging, or migrating data between topics.&lt;/p&gt;

&lt;p&gt;The initial MVP I scoped was simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Basic replay of messages with filters.&lt;/li&gt;
&lt;li&gt;Ability to dump Kafka topic data.&lt;/li&gt;
&lt;li&gt;Query dumped data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted it lightweight, scriptable, and easy to use - no streaming engine, web UI, or over-engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The first decision I had to make was whether to use Python or Golang.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Arguments for Python&lt;/strong&gt; - I have the most experience with it and expected it would be easier and faster to develop.&lt;br&gt;
&lt;strong&gt;Arguments for Golang&lt;/strong&gt; - In the long run, it would most likely be more performant. I would get more familiar with Golang.&lt;/p&gt;

&lt;p&gt;Due to my decision to have something tangible in a few days, I went with Python. Since it is a small tool and I didn’t know how much use it would get, I preferred not to worry about making it as performant as possible - premature optimization is the root of all evil, after all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools used for this project:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kafka - the core thing I wanted to learn. Using the &lt;code&gt;confluent_kafka&lt;/code&gt; Python package, as it had all the features I needed.&lt;/li&gt;
&lt;li&gt;DuckDB - see above.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://typer.tiangolo.com/" rel="noopener noreferrer"&gt;Typer&lt;/a&gt; - a library for building CLI applications. I had never used it before but liked the look and ergonomics it offered.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arrow.apache.org/docs/python/parquet.html" rel="noopener noreferrer"&gt;PyArrow for Parquet&lt;/a&gt; - efficient storage; I’m used to working with it, and DuckDB can read from it. For alternatives could have used JSON or Avro, but JSON is inefficient for larger data volumes. Avro - might add support in the future.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Dump Kafka topics into Parquet files&lt;/li&gt;
&lt;li&gt;Replay messages from Parquet back into Kafka&lt;/li&gt;
&lt;li&gt;Filter replays by timestamp range and key&lt;/li&gt;
&lt;li&gt;Optional throttling during replay&lt;/li&gt;
&lt;li&gt;Apply custom transform hooks to modify or skip messages&lt;/li&gt;
&lt;li&gt;Preview replays without sending messages using &lt;code&gt;--dry-run&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Control output verbosity with &lt;code&gt;--verbose&lt;/code&gt; and &lt;code&gt;--quiet&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Query message dumps with DuckDB SQL&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Kafka&lt;/strong&gt; - Not as intimidating as expected and quite enjoyable. Both the official Kafka CLI tools and the Python integrations are mature.&lt;br&gt;
&lt;strong&gt;DuckDB&lt;/strong&gt; - Currently limited use in the project, but good for what it does. I might add more use for it in the future or remove it to reduce bloat if it isn’t utilized.&lt;br&gt;
&lt;strong&gt;Typer&lt;/strong&gt; - Enjoyed working with it a lot. Super easy to get a CLI tool going.&lt;br&gt;
&lt;strong&gt;Testing&lt;/strong&gt; - Used &lt;code&gt;pytest&lt;/code&gt;. For unit tests, I didn’t want Kafka running for each test, so I used &lt;code&gt;MagicMock&lt;/code&gt; and &lt;code&gt;monkeypatch&lt;/code&gt; to simulate real objects - techniques I’ll keep in my pocket for future. For integration testing, I spun up a Docker container with a Kafka broker to test real usage of the CLI using &lt;code&gt;subprocess&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Main takeaway:&lt;/strong&gt;&lt;br&gt;
It’s important to figure out your goals and think about the architecture before you start mashing on the keyboard. Deciding the project scope and dependencies early let me focus on the main features. It’s always a balancing act: what’s core, what’s nice to have, and how much time you want to spend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outcome &amp;amp; Reflection
&lt;/h2&gt;

&lt;p&gt;Did I get more Kafka experience? Yes.&lt;br&gt;
Does the tool do what I set out to make it do? Yes.&lt;br&gt;
Is it the best thing since sliced bread? Highly unlikely.&lt;br&gt;
Are there better tools for this use case? Probably.&lt;/p&gt;

&lt;p&gt;At the end of the day, this was a learning experience and I had fun. If someone uses it - great. If no one does - also great, it just means that I didn't spend enough time researching real usage problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation &amp;amp; Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;kafka-replay-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kafka-replay-cli dump &lt;span class="nt"&gt;--help&lt;/span&gt;
kafka-replay-cli replay &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Thank you for reading.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Kafka Producers Explained: Partitioning, Batching, and Reliability</title>
      <dc:creator>Konstantinas Mamonas</dc:creator>
      <pubDate>Mon, 28 Apr 2025 11:06:21 +0000</pubDate>
      <link>https://forem.com/konstantinas_mamonas/kafka-producers-explained-partitioning-batching-and-reliability-4bm8</link>
      <guid>https://forem.com/konstantinas_mamonas/kafka-producers-explained-partitioning-batching-and-reliability-4bm8</guid>
      <description>&lt;p&gt;A Kafka producer is the entry point for all data written to Kafka. It sends records to specific topic partitions, defines batching behavior, and controls how reliably data is delivered.&lt;/p&gt;

&lt;p&gt;This post covers the behaviors and configurations that influence the producer: partitioning, batching, delivery guarantees, and message structure.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Does a Kafka Producer Do?
&lt;/h2&gt;

&lt;p&gt;A Kafka producer is a client library integrated into applications to write messages to Kafka topics. When a message is sent, the producer determines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which partition the message should go to
&lt;/li&gt;
&lt;li&gt;How to serialize the message for Kafka
&lt;/li&gt;
&lt;li&gt;Whether to batch it with others
&lt;/li&gt;
&lt;li&gt;How many acknowledgments are required before the message is considered delivered
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Producers are designed to balance speed, reliability, ordering, and throughput. Optimizing for one might require to compromise another.&lt;/p&gt;




&lt;h2&gt;
  
  
  Partitioning Strategies: Routing Messages to Partitions
&lt;/h2&gt;

&lt;p&gt;Kafka topics are split into partitions. Every message sent by a producer is written to one partition. This decision is made by a partitioner function.&lt;/p&gt;

&lt;h3&gt;
  
  
  With a Key
&lt;/h3&gt;

&lt;p&gt;If a message has a key, Kafka hashes it using the Murmur2 algorithm and assigns the message to a partition using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;partition = hash(key) % number_of_partitions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures all messages with the same key go to the same partition. Kafka guarantees message order within a partition, so key-based partitioning is how per-key ordering is maintained.&lt;/p&gt;

&lt;h3&gt;
  
  
  Without a Key
&lt;/h3&gt;

&lt;p&gt;If the key is null, Kafka uses one of two strategies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Round-robin&lt;/strong&gt;: messages cycle through partitions in order. Used in older clients
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sticky partitioning&lt;/strong&gt;: the producer sends all messages to the same partition until the batch is sent, then picks a new one. Default in modern clients
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sticky partitioning improves batching efficiency while maintaining fair distribution over time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Message Format: Structure and Serialization
&lt;/h2&gt;

&lt;p&gt;Kafka treats every message as a set of bytes. Each record includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Key (optional): used for partitioning. Serialized to bytes
&lt;/li&gt;
&lt;li&gt;Value: the actual data payload. Serialized to bytes
&lt;/li&gt;
&lt;li&gt;Headers (optional): metadata as key-value pairs
&lt;/li&gt;
&lt;li&gt;Timestamp: assigned by the client or broker
&lt;/li&gt;
&lt;li&gt;Partition + Offset: assigned by the broker after the message is stored
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kafka does not interpret or modify message content; it just stores and transmits byte arrays. Producers are responsible for serializing messages before sending them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example (Python):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;producer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KafkaProducer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value_serializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Efficient serialization improves throughput and reduces broker load. Avoid inefficient formats like uncompressed JSON unless specifically required by system constraints.&lt;/p&gt;




&lt;h2&gt;
  
  
  Batching and Compression: Optimizing Throughput
&lt;/h2&gt;

&lt;p&gt;Sending one message per request is inefficient. Kafka producers batch multiple records together per partition before sending them to the broker.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Configuration Options
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;batch.size&lt;/code&gt;: maximum size in bytes for a batch. Larger batches improve compression and throughput, but increase memory usage
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;linger.ms&lt;/code&gt;: how long to wait before sending a batch, even if it is not full. Increases batching opportunities at the cost of latency
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;compression.type&lt;/code&gt;: compresses full batches. Options include &lt;code&gt;gzip&lt;/code&gt;, &lt;code&gt;lz4&lt;/code&gt;, &lt;code&gt;snappy&lt;/code&gt;, &lt;code&gt;zstd&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;send()&lt;/code&gt; method is non-blocking. It queues the record in memory and returns immediately. The background sender thread flushes batches when &lt;code&gt;batch.size&lt;/code&gt; is reached or &lt;code&gt;linger.ms&lt;/code&gt; expires.&lt;/p&gt;

&lt;p&gt;Batching operates on a per-partition basis. As a result, applications that produce to a large number of partitions may experience reduced batching efficiency unless message flow is concentrated across fewer partitions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Delivery Guarantees: Configuring Reliability and Ordering
&lt;/h2&gt;

&lt;p&gt;Kafka producers can trade reliability for speed using the &lt;code&gt;acks&lt;/code&gt; configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;acks=0&lt;/code&gt;: fire and forget. Fastest, but data may be lost
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;acks=1&lt;/code&gt;: wait for leader. Reasonable balance for many use cases
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;acks=all&lt;/code&gt;: wait for all in-sync replicas. Safest, with higher latency
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Ordering and Retries
&lt;/h3&gt;

&lt;p&gt;Kafka guarantees ordering within a single partition. To maintain strict ordering, ensure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All related messages share the same key
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max.in.flight.requests.per.connection &amp;lt;= 1&lt;/code&gt; when retries are enabled (to prevent out-of-order writes during retries)
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Idempotence and Transactions
&lt;/h2&gt;

&lt;p&gt;By default, producers use at-least-once semantics, meaning retries may cause duplicate messages. Kafka provides stronger guarantees where needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Idempotent Producer
&lt;/h3&gt;

&lt;p&gt;Enable with &lt;code&gt;enable.idempotence=true&lt;/code&gt;. This prevents duplicates during retries by assigning each producer a unique ID and tracking sequence numbers per partition.&lt;/p&gt;

&lt;p&gt;This guarantees exactly-once delivery per partition, assuming the producer does not crash and restart with a new ID.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use this when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Downstream systems cannot deduplicate
&lt;/li&gt;
&lt;li&gt;Every message must be uniquely written (for example, financial systems)
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid using high &lt;code&gt;max.in.flight&lt;/code&gt; values with idempotence if ordering matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transactional Producer
&lt;/h3&gt;

&lt;p&gt;Transactional producers enable atomic writes across multiple partitions or topics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requires:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A configured &lt;code&gt;transactional.id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use of API methods: &lt;code&gt;begin_transaction()&lt;/code&gt;, &lt;code&gt;send()&lt;/code&gt;, &lt;code&gt;commit_transaction()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is critical for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exactly-once event processing pipelines
&lt;/li&gt;
&lt;li&gt;Kafka Streams applications
&lt;/li&gt;
&lt;li&gt;Coordinating multiple topic writes as a single atomic unit
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Transactions ensure no duplicates, no partial writes, and consistent failure handling.&lt;/p&gt;




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

&lt;p&gt;A well-tuned Kafka producer is critical to balancing throughput, reliability, and resource efficiency. It's important to understand your delivery requirements and system constraints before leaning into aggressive batching or strong guarantees as you trade higher throughput for it.&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>architecture</category>
      <category>performance</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>Kafka Consumers Explained: Pull, Offsets, and Parallelism</title>
      <dc:creator>Konstantinas Mamonas</dc:creator>
      <pubDate>Wed, 23 Apr 2025 09:39:35 +0000</pubDate>
      <link>https://forem.com/konstantinas_mamonas/kafka-consumers-explained-pull-offsets-and-parallelism-iki</link>
      <guid>https://forem.com/konstantinas_mamonas/kafka-consumers-explained-pull-offsets-and-parallelism-iki</guid>
      <description>&lt;p&gt;Kafka is built for high throughput, scalability, and fault tolerance. At the core of this is its consumer model. Unlike traditional messaging systems, Kafka gives consumers full control over how they read data. This post explains how Kafka consumers work by focusing on three things: how they pull data, how offsets work, and how parallelism is achieved with consumer groups.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pulling Data from Kafka
&lt;/h2&gt;

&lt;p&gt;Kafka producers push data to brokers. Consumers pull data from brokers. This setup is intentional. It gives consumers control over how fast they process data.&lt;/p&gt;

&lt;p&gt;In push-based systems, if the producer is faster than the consumer, the consumer can get overwhelmed or crash. Kafka avoids this problem by letting consumers decide when to fetch data. This helps with backpressure and makes the system more reliable.&lt;/p&gt;

&lt;p&gt;Pulling also helps with batching. A consumer can fetch many messages in a single request. This reduces the number of network calls. In contrast, push systems must send each message one by one or hold back messages without knowing if the consumer is ready.&lt;/p&gt;

&lt;p&gt;One downside of pull-based systems is wasteful polling. A consumer might keep asking for data even if nothing is available. Kafka avoids this by letting the consumer wait until enough data is ready before responding. This keeps CPU usage low and throughput high.&lt;/p&gt;

&lt;p&gt;Kafka also avoids a model where brokers pull data from producers. That design would need every producer to store its own data. It would require more coordination and increase the risk of disk failure. Instead, Kafka stores data on the broker, where it can be managed and replicated more easily.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Kafka Offsets Work
&lt;/h2&gt;

&lt;p&gt;Kafka splits topics into partitions. Each message in a partition has a number called an offset. The offset marks the position of the message in the log.&lt;/p&gt;

&lt;p&gt;Offsets give consumers control. A consumer chooses where to start reading and can track what has already been processed. If a consumer crashes, it can pick up where it left off by using its last committed offset.&lt;/p&gt;

&lt;p&gt;Kafka does not track this progress for the consumer. The consumer is responsible for managing its own offsets. This is part of what makes Kafka scalable and efficient.&lt;/p&gt;

&lt;p&gt;By default, a consumer starts at offset zero. This means it will read all messages that are still available. It can also be configured to start at the latest offset to only read new data.&lt;/p&gt;

&lt;p&gt;Kafka only keeps data for a limited time. If a consumer tries to read from an offset that is too old, Kafka will return an error. In that case, the consumer must reset to the earliest or latest available offset.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Terms
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Offset&lt;/strong&gt;: The position of a message in a partition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log End Offset&lt;/strong&gt;: The offset where the next message will be written.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Committed Offset&lt;/strong&gt;: The offset of the last message the consumer has finished processing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Delivery Options
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;At-most-once&lt;/strong&gt;: The consumer commits the offset before processing. If it crashes during processing, the message is lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At-least-once&lt;/strong&gt;: The consumer commits the offset after processing. If it crashes before committing, the message may be processed again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exactly-once&lt;/strong&gt;: This uses Kafka transactions. The message and its offset are written together. If anything fails, nothing is committed. This guarantees no duplication and no loss.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Parallelism with Consumer Groups
&lt;/h2&gt;

&lt;p&gt;Kafka uses consumer groups to scale out processing. A consumer group is a set of consumers working together to read from a topic.&lt;/p&gt;

&lt;p&gt;Kafka assigns each partition to only one consumer in the group. This avoids duplication and ensures order within each partition.&lt;/p&gt;

&lt;p&gt;When the group changes (for example, when consumers are added or removed), Kafka reassigns partitions to the available consumers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Examples
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;100 partitions and 100 consumers: each consumer handles one partition.&lt;/li&gt;
&lt;li&gt;100 partitions and 50 consumers: each consumer handles two partitions.&lt;/li&gt;
&lt;li&gt;50 partitions and 100 consumers: only 50 consumers do work, the rest are idle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kafka does not let multiple consumers read from the same partition in the same group. This would require the broker to manage shared state, which adds complexity. Instead, Kafka puts the responsibility on the consumer to track offsets. This makes the broker faster and simpler.&lt;/p&gt;

&lt;p&gt;The number of partitions controls how much you can scale out. More partitions allow for more parallelism. Choosing the right number of partitions is important for performance and resource usage.&lt;/p&gt;




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

&lt;p&gt;Kafka gives consumers control over how they pull data, where they start, and how they scale. Pull-based reads avoid overload. Offsets make it easy to recover from failure. Consumer groups allow you to scale out processing.&lt;/p&gt;

&lt;p&gt;This design makes Kafka fast, reliable, and efficient at any scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  Appendix: Quick Reference
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Partition&lt;/strong&gt;: A subset of a topic. Used for parallel processing and message ordering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offset&lt;/strong&gt;: A number showing a message’s position in a partition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumer Group&lt;/strong&gt;: A set of consumers that share the work of reading from a topic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebalancing&lt;/strong&gt;: The process where Kafka reassigns partitions when consumers join or leave a group.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery Types&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;At-most-once&lt;/em&gt;: Fast, but may lose messages.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;At-least-once&lt;/em&gt;: Reliable, but may duplicate messages.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Exactly-once&lt;/em&gt;: Most accurate, but needs Kafka transactions.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>eventdriven</category>
      <category>architecture</category>
      <category>streaming</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>How Kafka Achieves High Throughput: A Breakdown of Its Log-Centric Architecture</title>
      <dc:creator>Konstantinas Mamonas</dc:creator>
      <pubDate>Sun, 20 Apr 2025 05:14:20 +0000</pubDate>
      <link>https://forem.com/konstantinas_mamonas/how-kafka-achieves-high-throughput-a-breakdown-of-its-log-centric-architecture-3i7k</link>
      <guid>https://forem.com/konstantinas_mamonas/how-kafka-achieves-high-throughput-a-breakdown-of-its-log-centric-architecture-3i7k</guid>
      <description>&lt;p&gt;Kafka routinely handles millions of messages per second on commodity hardware. This performance isn't accidental. It stems from deliberate architectural choices centered around log-based storage, OS-level optimizations, and minimal coordination between readers and writers.&lt;/p&gt;

&lt;p&gt;This post breaks down the core mechanisms that enable Kafka's high-throughput design.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Append-Only Log Storage
&lt;/h2&gt;

&lt;p&gt;Each Kafka topic is split into partitions, and each partition is an append-only log. It is essentially a durable, ordered sequence of messages that are immutable once written.&lt;/p&gt;

&lt;p&gt;To manage growing data size efficiently, Kafka breaks each partition’s log into multiple segment files. A segment is a file on disk that stores a continuous range of messages. New messages are always written to the active segment using low-level system calls like &lt;code&gt;write()&lt;/code&gt;. This write lands in the OS page cache, not written to disk immediately.&lt;/p&gt;

&lt;p&gt;Kafka delays calling &lt;code&gt;fsync()&lt;/code&gt; to flush data to disk, relying instead on configurable flush policies (based on time or size). This reduces disk I/O and improves performance, at the cost of brief durability gaps. Kafka mitigates this through replication across brokers.&lt;/p&gt;

&lt;p&gt;Over time, when a segment reaches a size threshold, it is closed and a new one is created. Older segments become read-only and are subject to log retention, compaction, or deletion based on topic settings.&lt;/p&gt;

&lt;p&gt;By aligning its write path with sequential disk I/O, Kafka avoids random seeks entirely. This makes reads and writes fast and predictable, even on spinning disks, and scales well with data volume.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Outperforming Traditional Queues with Sequential I/O
&lt;/h2&gt;

&lt;p&gt;Traditional messaging systems often manage message delivery using per-consumer tracking and persistence mechanisms that can result in random disk access, especially during acknowledgment, redelivery, or crash recovery. While these systems are efficient in memory, random I/O patterns on disk introduce performance bottlenecks. For spinning disks, a single seek can take around 10 milliseconds, and disks can only perform one seek at a time.&lt;/p&gt;

&lt;p&gt;Kafka sidesteps this entirely by relying on sequential I/O. Writes are appended, and reads proceed in order. This design significantly improves disk efficiency, especially under load.&lt;/p&gt;

&lt;p&gt;By decoupling performance from data volume and enabling concurrent read/write access, Kafka makes efficient use of low-cost storage hardware, such as commodity SATA drives, without sacrificing performance.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Speeding Up Seeks with Lightweight Indexing
&lt;/h2&gt;

&lt;p&gt;Each segment file is accompanied by lightweight offset and timestamp indexes. These allow consumers to seek directly to specific message positions without scanning entire files, ensuring fast lookup even on large datasets.&lt;/p&gt;

&lt;p&gt;Since Kafka consumers track their own offsets and messages are immutable, there is no need to update shared state for acknowledgments or deletions. This eliminates coordination between readers and writers, reducing contention and enabling true parallelism.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Batching to Maximize I/O Efficiency
&lt;/h2&gt;

&lt;p&gt;High-throughput systems must avoid the overhead of processing one message at a time. Kafka uses a message set abstraction to batch messages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Producers group messages before sending them.&lt;/li&gt;
&lt;li&gt;Brokers perform a single disk write per batch.&lt;/li&gt;
&lt;li&gt;Consumers fetch large batches with a single network call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This batching reduces system calls, disk seeks, and protocol overhead. As a result, throughput improves significantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Zero-Copy Data Transfer with &lt;code&gt;sendfile()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Conventional data transfer involves multiple memory copies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Disk to kernel space (page cache)
&lt;/li&gt;
&lt;li&gt;Kernel to user space (application buffer)
&lt;/li&gt;
&lt;li&gt;User space back to kernel (socket buffer)
&lt;/li&gt;
&lt;li&gt;Kernel to NIC buffer (for network)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Kafka avoids this overhead using the &lt;code&gt;sendfile()&lt;/code&gt; system call. This enables zero-copy data transfer from the page cache directly to the network stack, bypassing user space entirely.&lt;/p&gt;

&lt;p&gt;This reduces CPU usage and memory pressure, allowing near wire-speed data transfer even under heavy load.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Long-Term Retention Without Performance Loss
&lt;/h2&gt;

&lt;p&gt;Kafka’s append-only log model enables long-term message retention, even for days or weeks, without degrading performance. Because reads and writes are decoupled, and messages are not mutated post-write, old data remains accessible without impacting current workloads.&lt;/p&gt;

&lt;p&gt;This supports powerful use cases like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replaying messages for state recovery
&lt;/li&gt;
&lt;li&gt;Late-arriving consumer processing
&lt;/li&gt;
&lt;li&gt;Time-travel debugging and auditing&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;Kafka’s high throughput is the result of system and architectural decisions that work together by design. Its log-centric model avoids random I/O, minimizes coordination, and takes full advantage of OS-level features like the page cache and zero-copy transfers.&lt;/p&gt;

&lt;p&gt;The result: Kafka handles massive data volumes not through abstract complexity, but by working with the OS instead of against it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Appendix: Key Terms
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;write()&lt;/code&gt;: A system call that transfers data from user space to the OS page cache.
&lt;/li&gt;
&lt;li&gt;Page cache: A memory buffer managed by the kernel.
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fsync()&lt;/code&gt;: Forces data in the page cache to be flushed to disk.
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sendfile()&lt;/code&gt;: A system call that sends data from a file directly to the network without copying to user space.
&lt;/li&gt;
&lt;li&gt;Sequential I/O: Reading or writing data in a linear order. Much faster than random I/O, especially on HDDs.
&lt;/li&gt;
&lt;li&gt;Random I/O: Accessing data at non-contiguous disk locations. This causes performance degradation due to disk seeks.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Further Reading:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kafka.apache.org/documentation/#design" rel="noopener noreferrer"&gt;Kafka Official Design Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>kafka</category>
      <category>architecture</category>
      <category>performance</category>
      <category>distributedsystems</category>
    </item>
  </channel>
</rss>
