<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Siddharth Khuntwal on Medium]]></title>
        <description><![CDATA[Stories by Siddharth Khuntwal on Medium]]></description>
        <link>https://medium.com/@skhuntwal123?source=rss-1055da364ce7------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*N1T28vUBTEAMJQD2</url>
            <title>Stories by Siddharth Khuntwal on Medium</title>
            <link>https://medium.com/@skhuntwal123?source=rss-1055da364ce7------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Mon, 06 Jul 2026 18:11:54 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@skhuntwal123/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[How We Reduced Redis Memory by 70% with One Small Change]]></title>
            <link>https://skhuntwal123.medium.com/how-we-reduced-redis-memory-by-70-with-one-small-change-06db08b37988?source=rss-1055da364ce7------2</link>
            <guid isPermaLink="false">https://medium.com/p/06db08b37988</guid>
            <category><![CDATA[redis]]></category>
            <category><![CDATA[system-design-concepts]]></category>
            <category><![CDATA[system-design-interview]]></category>
            <category><![CDATA[optimization]]></category>
            <category><![CDATA[data-compression]]></category>
            <dc:creator><![CDATA[Siddharth Khuntwal]]></dc:creator>
            <pubDate>Sun, 28 Jun 2026 14:01:13 GMT</pubDate>
            <atom:updated>2026-06-28T14:01:13.349Z</atom:updated>
            <content:encoded><![CDATA[<h4><strong>The accidental discovery that led us to LZ4 and a 70% drop in Redis memory usage</strong></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M6izhV4V4AhCIknSawpWjw.png" /></figure><h3>The Problem: Redis Was Getting Expensive, and Nobody Had Noticed</h3><p><strong>It didn’t start as a Redis problem. </strong>I was investigating a high-latency endpoint. One of those tasks where you pull on a thread expecting to find a slow query or an N+1, and end up somewhere completely different. While profiling the request path, I pulled up our Redis instance metrics to check cache behaviour. That’s when it caught my eye.</p><p>We were running a large Redis instance, significantly over-provisioned relative to what we actually needed. Nobody had raised an alarm because the cache <em>worked</em>. Hit rates were healthy, latency was acceptable, and the infrastructure cost had grown gradually enough that it never triggered a conversation. <em>Classic boiling frog.</em></p><p>Digging deeper, the root cause was straightforward: we were inserting raw JSON directly into Redis: verbose, deeply nested, full of repetitive field names. Across hundreds of thousands of keys with a 24-hour TTL, that adds up fast. We were consuming close to <strong>~40 GB of memory</strong> for data that, fundamentally, didn’t need to be that large.</p><p>The high-latency investigation got parked. This was the bigger problem.</p><p>The options on the table were:</p><ol><li><strong>Increase instance size: </strong>easy, but a band-aid.</li><li><strong>Reduce TTLs: </strong>would hurt cache hit rates.</li><li><strong>Prune cached fields: </strong>risky without a full audit of all consumers.</li><li><strong>Compress the values: </strong>low risk, high upside, no application changes required.</li></ol><p>We went with option 4.</p><h3>Choosing the Right Compression Algorithm</h3><p>Not all compression is the same. Before writing a single line of code, we spent time understanding the trade-off space. Picking the wrong algorithm for a cache layer can make things worse, not better.</p><h3>The Contenders</h3><p><strong>Gzip (DEFLATE): </strong>The default choice for a lot of engineers because it’s everywhere. gzip combines LZ77 with Huffman coding to achieve high compression ratios. The problem is speed. gzip is optimised for ratio, not throughput. Compression can take several milliseconds on a moderately sized JSON payload. For files or HTTP transfer, that’s fine. For a hot cache path executing thousands of times per second, it’s a tax you can’t afford.</p><p><strong>Zstd (Zstandard): </strong>Meta’s algorithm, designed specifically to close the gap between ratio and speed. At its default level, zstd gets close to gzip’s compression ratio at 5–10x the speed. It also supports dictionary-based compression, which can push ratios even higher for data with shared structure. zstd was a serious candidate for us.</p><p><strong>Snappy: </strong>Google’s algorithm, built for speed over ratio. Snappy explicitly doesn’t aim for maximum compression. It targets very fast compression with reasonable results and is widely used in systems like LevelDB, Cassandra, and Hadoop. The downside: compression ratios are lower than LZ4 on JSON data, and Python ecosystem support is thinner.</p><p><strong>LZ4: </strong>Pure speed. LZ4 is designed to saturate memory bandwidth rather than CPU cycles. It can compress and decompress at multiple GB/s on modern hardware. Compression ratio is in the same ballpark as Snappy but generally better on structured text data like JSON.</p><h3>Why We Chose LZ4</h3><ul><li><strong>Decompression speed matters more than compression speed.</strong> Every cache read triggers a decompression. Every cache write triggers a compress. But in a read-heavy cache, decompression runs far more often. LZ4’s decompression at ~4 GB/s is essentially free at our scale, adding single-digit microseconds per operation.</li><li><strong>The ratio difference from zstd doesn’t justify the overhead.</strong> zstd would have given us perhaps 5–8% better compression on our payloads. That’s real, but not meaningful when LZ4 already gets us into the 60–70% reduction range. We’d be adding CPU overhead for diminishing returns.</li><li><strong>JSON compresses well regardless of the algorithm.</strong> Deeply nested JSON with repetitive field names and predictable structure doesn’t need a sophisticated algorithm to shrink significantly. Even a fast one like LZ4 tears through it. You don’t need to reach for gzip unless you’re optimising for absolute minimum storage with no latency constraints.</li></ul><p>LZ4 hits the sweet spot: <strong>55–70% size reduction on JSON payloads</strong> with compression and decompression times in the microsecond range.</p><h3>How We Implemented It</h3><p>The core idea is simple: compress before SET, decompress after GET. The goal was to make this completely invisible to all call sites. No application code should need to know compression exists.</p><p>Our codebase already had an BaseCacheHandler abstraction sitting over Redis, providing a consistent interface for every cache key. That was the clean insertion point.</p><p>We created a CompressedCacheHandler that extended the base class and wrapped every read and write with LZ4 compression. We paired LZ4 orjson instead of the standard json module. orjson is a Rust-backed JSON serializer that outputs bytes directly, which is exactly what LZ4 expects as input. One fewer allocation, faster serialisation, and cleaner code.</p><pre>def _compress(data: Any) -&gt; bytes:<br>    return lz4.frame.compress(orjson.dumps(data))<br><br>def _decompress(data: bytes) -&gt; Any:<br>    try:<br>        return orjson.loads(lz4.frame.decompress(data))<br>    except (RuntimeError, TypeError, orjson.JSONDecodeError):<br>        return data  # legacy uncompressed key, return as-is</pre><p>The fallback in _decompress handles backward compatibility. Since LZ4 frame bytes can never be mistaken for valid JSON, the failure is deterministic. Old uncompressed keys return safely, and as they expired through their natural 24-hour TTL, the mixed state resolved itself with zero intervention.</p><h3>Sharding</h3><p>We also run a multi-Redis-instance setup with consistent hashing. Rather than entangle sharding logic with compression logic, we composed them via multiple inheritance:</p><pre>class ShardedCompressedCacheHandler(CompressedCacheHandler, RendezvousSharding):<br>    def __init__(self, key: str, timeout: int, alias: list | str):<br>        shard = self._get_shard(alias, key) if isinstance(alias, list) else alias<br>        super().__init__(key, timeout, shard)</pre><p>Rendezvous hashing ensures that when a Redis node is added or removed, only the minimum number of keys are remapped. Keeping compression and sharding as separate layers meant both stayed independently testable.</p><h3>The Results</h3><p>We rolled out over the weekend. New writes came in compressed, and old keys expired through their TTL. By the end of the window, Redis memory had dropped by roughly <strong>70%</strong> with no measurable impact on latency or cache hit rates.</p><h3>What to Watch Out For</h3><p><strong>Small values don’t always compress well.</strong> LZ4 has frame overhead for very small inputs. If your cached values are under roughly 200 bytes, compression can actually increase size. Worth adding a minimum size threshold before compressing.</p><p><strong>The exception-based fallback is intentional, not lazy.</strong> Our approach of catching decompression failures to handle legacy keys is safe, specifically because LZ4 frame bytes can’t be mistaken for valid JSON. If you have existing keys in a format that could look like compressed data, use an explicit version prefix instead.</p><p><strong>Already-compressed or binary values need special handling.</strong> If you cache anything that isn’t plain JSON — protobuf, msgpack, base64-encoded blobs — the exception fallback will silently return raw bytes. Make sure downstream consumers can handle that, or add type detection before compressing.</p><p><strong>Monitor compression ratio per key namespace.</strong> Some data compresses dramatically. Other data barely shrinks. It’s worth logging the ratio per key prefix in staging to identify where you’re getting the most benefit and where compression is a waste of effort.</p><h3>Conclusion</h3><p>What started as a routine latency investigation turned into one of the highest-impact changes we shipped that quarter. We didn’t rewrite anything, didn’t change how data was structured, and didn’t touch a single call site. We just stopped storing data in an unnecessarily large format.</p><p>LZ4 compression on JSON-heavy Redis values is one of those rare optimisations where the risk is low, the effort is minimal, and the upside is significant. If you’re storing raw JSON in Redis at scale and haven’t looked at compression yet, it’s worth a few hours of your time.</p><p><em>If you’ve done something similar or hit edge cases I didn’t cover, I’d love to hear about it.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=06db08b37988" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[We Gave Our AI Agents the One Thing That Actually Moved Our Business Metrics]]></title>
            <link>https://skhuntwal123.medium.com/we-gave-our-ai-agents-the-one-thing-that-actually-moved-our-business-metrics-363c5d9f1bcc?source=rss-1055da364ce7------2</link>
            <guid isPermaLink="false">https://medium.com/p/363c5d9f1bcc</guid>
            <category><![CDATA[artificial-intelligence]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[systems-thinking]]></category>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Siddharth Khuntwal]]></dc:creator>
            <pubDate>Thu, 04 Jun 2026 16:20:07 GMT</pubDate>
            <atom:updated>2026-06-04T16:20:07.349Z</atom:updated>
            <content:encoded><![CDATA[<h4>Memory is the missing layer in most AI agent stacks. Here’s how we built ours.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Cwq6gKzx6hHU8pG6ku4UnQ.png" /></figure><h3>The State of AI Agent Memory Today</h3><p>In the past year, companies everywhere have been racing to deploy AI agents. Sales qualification, customer support, onboarding, and follow-up tasks that used to require a trained human are getting handed off to bots. <strong>And then the metrics don’t move!!</strong></p><p>The ROI doesn’t show up. Conversion stays flat. Support deflection looks fine on paper, but customer satisfaction quietly drops. Teams that were proud of what they built six months ago are now quietly wondering what went wrong. <br><strong>The usual diagnosis:</strong> <em>the AI can’t handle objections. It can’t reason through pushback. It can’t convince people the way a trained human rep can.</em></p><p><strong>That’s true, but <em>it’s the wrong diagnosis.</em></strong></p><p>A great human sales/support agent isn’t better because they’re human. They’re better because they have context. They remember that this person called in three weeks ago, frustrated about pricing. They know the company just changed plans. They caught a detail in a prior conversation that tells them exactly how to frame this one. <strong><em>They’re not smarter, they’re informed.</em></strong></p><p>Your AI agent isn’t losing because it’s a machine. It’s losing because it starts every single conversation knowing nothing.</p><p>Every interaction is <em>session zero</em>. The user who explained their situation last Tuesday? Never met them. The person who was almost ready to convert two weeks ago? Stranger. The customer who mentioned a very specific constraint that would’ve changed your entire pitch? Gone.</p><p>This isn’t a failure of the LLM. <em>It’s a failure of infrastructure.</em> And right now, most teams are solving it badly, or not solving it at all.</p><h3>The Context Window Is Not Memory</h3><p>The first thing teams try is history injection, pulling the last N conversations, stuffing them into the system prompt, and letting the model work it out.</p><p>This is understandable. It <em>sort of</em> works. And it creates four problems you don’t notice until you’re in production.</p><ul><li><strong>Cost:</strong> Every conversation now costs as much as all previous conversations combined. Ten sessions in, you’re passing ten sessions of text on every single request.</li><li><strong>Latency:</strong> More tokens mean a slower response. At scale, users feel this.</li><li><strong>Noise over signal:</strong> A raw transcript is mostly filler, pleasantries, false starts, and repetition. The facts buried in it, like who this person is, what they need, and what’s changed, don’t surface. <strong><em>You’re asking the model to read a document when it needs a briefing.</em></strong></li><li><strong>No cross-channel coherence:</strong> A user who spoke to your voice agent on Monday and opened a chat on Thursday is two separate people in your system. Their histories don’t connect. There’s no concept of identity, just sessions!!</li></ul><p>This is what we learned from our experience.<em> </em><strong><em>The context window is a working memory tool. It was never meant to be long-term memory.</em></strong></p><h3>Vector Search Alone Doesn’t Solve It Either</h3><p>The next step teams take is RAG, store conversation history in a vector database, retrieve relevant chunks at query time, and inject them as context. Better. But still incomplete.</p><ul><li><strong>You’re storing the wrong thing: </strong>Embedding raw transcripts means you’re searching through noise. The semantically relevant unit is not a paragraph of conversation; it’s an extracted fact. <strong><em>“User prefers async communication”</em></strong> is a better memory than three hundred words of a transcript that implies it.</li><li><strong>Retrieval is probabilistic:</strong> You might get the relevant memory. You might not. Whether an agent remembers something about a user depends on whether the right embedding surfaces at the right moment. <strong><em>That’s fragile.</em></strong></li><li><strong>No rollup:</strong> After fifty conversations with a user, you have fifty chunks in a vector store. Every time you need context, you’re re-synthesising who this person is from scratch. There’s no maintained, up-to-date summary, no single answer to <strong><em>“what do we know about this person today?”</em></strong></li><li><strong>No identity anchor:</strong> The vector store has no concept of a person. It has documents. Memories are scoped to whatever key you happened to use, often a session ID, not a stable identity.</li></ul><h3>There’s No Standard Memory Infrastructure</h3><p>The deeper issue is that memory for AI agents is treated as an application concern, not an infrastructure concern. Every team builds their own version.</p><p>A Redis key per user with a JSON blob that gets appended. A table of past summaries with no deduplication. A prompt template that injects “what we know” from a hand-maintained database.</p><p><em>These work in demos.</em> They break in different ways in production — duplication, drift, orphaned records, no idempotency when events replay, and no way to support multiple conversation sources without forking the logic. There’s no agreed-upon model for what memory is, how it gets updated, and how agents consume it.</p><h3>Here’s How We Solved It: Building Cerebro</h3><p>We built <strong>Cerebro</strong>, a memory microservice for AI agents. Named after Professor X’s machine from X-Men, similar to the movie, our service does the same; <em>it reaches into every past conversation and surfaces what an AI agent needs to know about a person, right now.</em></p><p>The goal was to solve each of these gaps with explicit design decisions, not workarounds.</p><h3>Anchor Memory to a Person, Not a Session</h3><p>The foundational shift: <em>memory belongs to a person, not a session.</em></p><p>Every ingest starts with resolving an Identity, a stable record keyed by phone number, email, or account ID. All memories attach to that identity. It doesn’t matter if the conversation came from voice, chat, or email. It doesn’t matter how many sessions there have been. The identity is the anchor.</p><p>When an agent needs context, it asks: <em>“What do we know about this person?” and </em>not <em>“What happened in these sessions?”</em></p><p><strong>This one change makes cross-channel memory coherent by design.</strong></p><h3>Store Facts, Not Transcripts</h3><p>We used <a href="https://mem0.ai/">mem0</a> for memory extraction. When a conversation ends, and the transcript arrives, mem0’s LLM pass extracts atomic facts from it:</p><blockquote>“Prefers remote work. Has 3 years of Python experience. Looking for a role in fintech.”</blockquote><p>These facts are embedded and stored in a vector database. <em>Not the transcript, the facts.</em> The vector store contains a signal, not noise.</p><p>But we don’t rely on retrieval alone. We maintain two main types of insight per identity:</p><ul><li><strong>EPISODE: </strong>What we learned from this specific conversation. Tied to that interaction, immutable. <em>A record of what happened.</em></li><li><strong>CURRENT STATE: </strong>A<strong> </strong>continuously updated rollup of everything we know about this person. One row. Always current. When a user’s situation changes, this row changes. When an agent needs context, it reads this row first.</li></ul><p><em>The current state solves the re-synthesis problem.</em> You never re-derive who a person is from fifty episodes. You maintain the answer.</p><h3>One Core, Many Sources</h3><p>Different conversation types need different extraction logic. A support chat and a sales call should produce different kinds of facts. The prompts, the field mappings, the interaction types — all different.</p><p>We solved this with YAML pipeline configs. Each source gets a file that declares what kind of interaction it represents and what extraction prompt to use. Cerebro routes each ingest through the right pipeline automatically.</p><p><em>Adding a new conversation source is adding a YAML file. The processing core doesn’t change.</em></p><h3>What Changed</h3><p><em>Before Cerebro</em>, agents started every conversation cold. No context. Users repeated themselves. Agents couldn’t reference prior interactions, couldn’t build on past conversations, couldn’t detect when something had changed.</p><p><em>After Cerebro</em>, every agent interaction starts with a rich, distilled profile of the person. The agent knows what has been discussed, what has changed, and what matters. Conversations feel like continuations, not restarts.</p><p>The shift isn’t just in user experience. It’s in what agents can <em>do</em>. An agent with persistent memory can detect change over time. It can flag when a user’s situation has changed since the last contact. It can personalise responses based on accumulated context, not just the current turn.</p><p>Memory is what turns a reactive agent into one that actually knows the people it’s working with.</p><h3>The Principles That Held</h3><p>If you’re building something similar, these are the decisions that mattered most.</p><ul><li>Anchor memory to identity, not sessions. <em>Sessions end. People don’t.</em></li><li>Store facts, not transcripts. <em>The value is in extraction, not retention.</em></li><li>Maintain a current state. <em>Don’t re-synthesise on every request; maintain the answer.</em></li><li>Build idempotency in from day one. <em>Duplicate events are not an edge case.</em></li><li>Separate the pipeline from the core. <em>New sources should be config changes, not code changes.</em></li></ul><p><strong><em>The human rep who outperforms your agent isn’t smarter. They just remember. Build agents that do the same.</em></strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=363c5d9f1bcc" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How Apache Flink Made Our Batch ETL Obsolete]]></title>
            <link>https://skhuntwal123.medium.com/how-apache-flink-made-our-batch-etl-obsolete-1efa30ac7e3b?source=rss-1055da364ce7------2</link>
            <guid isPermaLink="false">https://medium.com/p/1efa30ac7e3b</guid>
            <category><![CDATA[data-analysis]]></category>
            <category><![CDATA[etl]]></category>
            <category><![CDATA[data-processing]]></category>
            <category><![CDATA[apache-flink]]></category>
            <dc:creator><![CDATA[Siddharth Khuntwal]]></dc:creator>
            <pubDate>Sun, 20 Jul 2025 07:55:07 GMT</pubDate>
            <atom:updated>2025-07-20T08:27:54.340Z</atom:updated>
            <content:encoded><![CDATA[<h3>Rethinking ETL: How Apache Flink Transformed Our Data Pipeline</h3><h3><strong>Introduction</strong></h3><p>In today’s data-first landscape, organizations are under increasing pressure to make decisions in real time. While traditional batch ETL systems have served us well, they come with inherent limitations — data latency, operational complexity, and resource inefficiency. We faced this challenge head-on: a legacy ETL system with fixed batch windows, stale data, and brittle pipelines.</p><p>This post outlines our journey of replacing an aging batch ETL pipeline with Apache Flink, a powerful stream processing engine that enabled real-time data availability, lowered costs, and simplified our architecture.</p><h3><strong>The Bottlenecks of Batch ETL</strong></h3><p>We began with a conventional batch architecture that processed various datasets through scheduled jobs executed every 24 hours. Over time, this setup became a bottleneck due to several critical issues:</p><ul><li><strong>Data Latency</strong>: Business decisions were based on data that was already stale by the time it was available.</li><li><strong>Resource Spikes</strong>: Heavy jobs ran in parallel during narrow time windows, leading to massive load peaks and under-utilized resources the rest of the day.</li><li><strong>Maintenance Overhead</strong>: Each pipeline had its own logic, scheduling, monitoring, and alerting mechanisms.</li><li><strong>Poor Scalability</strong>: Scaling batch pipelines meant increasing compute power, which only delayed job runtimes further.</li><li><strong>Data Drift and Inconsistency</strong>: Syncing logic across multiple pipelines was error-prone and fragile.</li></ul><p>We realized this architecture could no longer support the dynamic, high-volume, low-latency data demands of our organization.</p><h3><strong>Why Apache Flink?</strong></h3><p>We evaluated multiple stream processing frameworks, including Apache Spark Streaming, Kafka Streams, and Flink. Apache Flink stood out due to its architectural elegance and maturity in stream-first processing.</p><ul><li><strong>Stream-First Architecture</strong>: Unlike micro-batch engines, Flink operates on true streams. Every event is processed as it arrives, unlocking low-latency computation.</li><li><strong>Exactly-Once Semantics</strong>: Flink’s stateful stream processing and checkpointing give strong consistency guarantees.</li><li><strong>Backpressure Management</strong>: Flink automatically detects slow downstream operators and propagates backpressure to throttle the source, preventing overload.</li><li><strong>Rich SQL &amp; CEP Support</strong>: Flink offers powerful abstractions for developers and analysts, including SQL, Table API, and Complex Event Processing.</li><li><strong>Unified Batch + Stream API</strong>: Flink handles batch as a special case of streaming, reducing cognitive load and allowing us to gradually migrate legacy jobs.</li></ul><h3><strong>New Architecture: Event-Driven and Stream-Native</strong></h3><p>Our reimagined data platform includes three key components:</p><ul><li><strong>Kafka</strong>: Acts as the event buffer, decoupling producers and consumers. All incoming data (logs, transactions, events) is published to Kafka topics.</li><li><strong>Apache Flink</strong>: The heart of the system. We built multiple Flink jobs, each consuming one or more topics, performing enrichment, validation, filtering, aggregations, and joining across streams.</li><li><strong>PostgreSQL</strong>: Used as the final sink for storing processed and transformed data, which is then used by downstream analytics tools and internal dashboards.</li></ul><p>Each Flink job runs continuously and reacts to data as it arrives, providing near-instant availability of information.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*f6tiyeYWdyobeAE1wLLvWg.png" /></figure><h3><strong>How Flink Solved Our Pain Points</strong></h3><p>The shift to Apache Flink addressed our major pain points:</p><ol><li><strong>Latency Dropped from Hours to Seconds</strong>: Business metrics and KPIs are now updated in real-time, enabling operational decisions based on the current state.</li><li><strong>Smarter Resource Utilization</strong>: Instead of concentrating all compute in a nightly window, Flink distributes compute needs evenly throughout the day.</li><li><strong>Unified Pipelines</strong>: We now manage significantly fewer jobs, each built on a shared abstraction with common monitoring and alerting.</li><li><strong>Scalability by Design</strong>: Adding a new stream or increasing throughput is as simple as scaling Flink clusters horizontally.</li><li><strong>Improved Data Quality</strong>: With built-in stateful processing, we implemented deduplication, out-of-order handling, and anomaly detection within the stream jobs.</li></ol><h3><strong>Impact Metrics</strong></h3><p>The impact of the migration was measurable and immediate:</p><ul><li><strong>Latency</strong>: Reduced from 24 hours to sub-10 seconds</li><li><strong>Throughput</strong>: 5x increase in events handled per second</li><li><strong>Infrastructure Cost</strong>: 30% reduction in compute costs due to even load distribution</li><li><strong>Operational Load</strong>: 50% fewer on-call alerts and data quality incidents</li></ul><h3><strong>Conclusion</strong></h3><p>Transitioning from batch ETL to Apache Flink was not just a technical upgrade — it was a paradigm shift. We moved from passive, delayed insights to a proactive, real-time data ecosystem.</p><p>Flink enabled us to build a system that is not only faster and cheaper but also fundamentally better suited to the dynamic nature of modern data.</p><p>If you’re operating with stale data and struggling with complex batch jobs, it might be time to consider the real-time revolution. We did — and we’re not looking back.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1efa30ac7e3b" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>