<?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[Wise Engineering - Medium]]></title>
        <description><![CDATA[Posts from the @Wise Engineering Team - Medium]]></description>
        <link>https://medium.com/wise-engineering?source=rss----f2565bbe9c46---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Wise Engineering - Medium</title>
            <link>https://medium.com/wise-engineering?source=rss----f2565bbe9c46---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 12 Nov 2026 22:36:44 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/wise-engineering" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[From Onboarding to Production: How I Built Dynamic Vendor Switching at Wise]]></title>
            <link>https://medium.com/wise-engineering/from-onboarding-to-production-how-i-built-dynamic-vendor-switching-at-wise-876942d6031f?source=rss----f2565bbe9c46---4</link>
            <guid isPermaLink="false">https://medium.com/p/876942d6031f</guid>
            <category><![CDATA[engineering-culture]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[payments]]></category>
            <category><![CDATA[backend]]></category>
            <dc:creator><![CDATA[rahman kapucu]]></dc:creator>
            <pubDate>Tue, 03 Nov 2026 09:52:56 GMT</pubDate>
            <atom:updated>2026-11-03T09:52:55.060Z</atom:updated>
            <content:encoded><![CDATA[<p>When I joined Wise, I was expecting the standard new-joiner experience: documentation, shadowing sessions, maybe a small bug fix or two to ease in. What I got instead was a real problem, a blank design doc, and a team ready to help me solve it.</p><p>That problem became my onboarding project, a system that automatically switches payment vendors when one of them starts failing, and that now quietly handles real customer payments every day.</p><p>Here’s the short version of what I learned: <strong>the switching decision itself is simple. The hard part and what makes it trustworthy is everything around it:</strong> choosing the right signals, validating in shadow mode, making every decision observable, and being conservative about what counts as “the same bank”.</p><p>The rest of this post is the long version.</p><h3>Some context</h3><p>Wise lets customers pay directly from their bank account. For EUR pay-ins, we integrate with external vendors that act as the bridge between Wise and a large number of European banks.</p><p>Many banks are reachable through more than one vendor. That overlap is the key to everything that follows.</p><h3>The problem nobody wanted to keep solving manually</h3><p>When one of our vendors has an issue, even a subtle one that never makes it onto a status page, payments start failing. A customer goes through the whole authorisation flow at their bank, comes back to Wise, and the payment doesn’t go through.</p><p>Until now, responding to this needed a human: someone had to notice the failures, work out which vendor and bank were affected, and intervene by hand. That’s slow, it puts pressure on whoever is on call, and customers could feel the problem before we do.</p><p>The goal of my project was to let the system detect and respond to these situations on its own.</p><ul><li><strong>Primary goal:</strong> automatically switch EUR pay-in traffic from one vendor to another, per bank, when one underperforms.</li><li><strong>Secondary goal:</strong> give the team visibility into vendor degradations that were previously harder to spot.</li><li><strong>Out of scope:</strong> redesigning our existing downtime detection platform, or extending this to recurring payments.</li></ul><h3>Step 1: Understand what already exists</h3><p>Before writing any code, I spent time understanding the existing system. Wise already had downtime detection for GBP Open Banking payments, which identifies degraded banks and disables them. That gave me a mental model and patterns to build on.</p><p>For EUR, nothing equivalent existed. No automated detection, no health classification, no automated response.</p><p>I also needed to understand vendor coverage: which banks each vendor supports, and where they overlap. A bank reachable through only one vendor can’t be switched. A bank reachable through several is a candidate.</p><h3>Step 2: Choose signals that reflect customer reality</h3><p>The design question was: <em>what should trigger a switch?</em></p><p>The obvious answer is “the vendor’s status page”. But vendors don’t always declare partial degradations. A vendor can show “all systems operational” while a meaningful share of payments for one specific bank is failing. Status pages reflect declared incidents, not necessarily what customers are experiencing.</p><p>So I designed two independent signals.</p><p><strong>Signal 1 : Vendor status (declared outages).</strong> One vendor sends us webhooks when a bank’s status changes. The other exposes a health API that we poll on a schedule. Both paths write to the same availability store. This signal is fast and authoritative, but only for problems the vendor acknowledges.</p><p><strong>Signal 2 : Conversion rate (actual customer outcomes).</strong> For every vendor × bank pair, we track what fraction of initiated payments reach a successful end state within a rolling time window, using a sorted set in Redis. Each bank has its own expected baseline, because banks differ in what “normal” looks like. If the observed rate drops far enough below that baseline, the pair is classified as degraded.</p><p>The second signal is what made the system genuinely useful. It reacts to what customers are experiencing, not to what a dashboard says.</p><p>The two signals cover each other’s blind spots: Signal 1 is fast but misses soft failures; Signal 2 takes longer to build up evidence, but catches exactly what Signal 1 misses.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AfTui7BsnA1i_1OsibpckA.png" /></figure><h3>Two details that matter more than they look</h3><p><strong>A minimum sample size.</strong> Without a floor on the number of payments in the window, one failed payment during a quiet hour can tank the conversion rate and trigger a switch. Below the floor, Signal 2 simply stays silent.</p><p><strong>Failing safe.</strong> If anything goes wrong while calculating the conversion rate, Redis is unreachable, there’s no baseline configured, not enough data, the tracker answers “not degraded”. When the signal itself is unreliable, the system does nothing rather than something.</p><h3>Step 3: Make the decision on every payment</h3><p>Both signals feed into a three-state health classification: <strong>UNAVAILABLE</strong>, <strong>DEGRADED</strong>, or <strong>HEALTHY</strong>. A declared outage takes priority, if the vendor says a bank is down, we don’t need to wait for conversion data to confirm it.</p><p>The check runs <strong>on every payment request</strong>, not on a background schedule. When a customer confirms a payment, we evaluate the health of their bank’s vendor at that moment. If it’s unhealthy and a healthy alternative exists, we switch before the payment is created.</p><p>What happens when there’s <em>no</em> healthy alternative depends on how bad things are:</p><ul><li>If the bank is <strong>UNAVAILABLE</strong>, the customer sees a clear error rather than a payment we know will fail.</li><li>If the bank is only <strong>DEGRADED</strong>, the payment still goes through the primary vendor. A degraded vendor still succeeds for most customers, and blocking the bank would make things worse, not better.</li></ul><p>That principle , <em>never make the customer’s situation worse than doing nothing</em> , shaped most of the design.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*v1hBpPhgesAKVuF1_-RCoA.png" /></figure><h3>Being conservative about “the same bank”</h3><p>We only switch to an alternative vendor explicitly configured for the same bank the customer selected. The customer’s selected bank remains unchanged. An alternative also has to pass every check the original would have: it must be healthy, supported for that payment amount and account type, and available to that customer.</p><p>An incorrect switch, sending a customer to a different institution than the one they chose is far worse than leaving them on a degraded vendor. So the mapping is small, explicit and deliberately cautious.</p><h3>Returning traffic to the primary vendor</h3><p>Once traffic moves to the alternative, the primary vendor stops receiving payments for that bank. Over time its window empties out, drops below the minimum sample size, and Signal 2 goes quiet. The bank is classified as healthy again and traffic returns to the primary. If the problem is still there, the signal builds up again and traffic moves back.</p><p>There’s no separate “recovery” logic. The sliding window works as a natural, gradual probe.</p><h3>Step 4: Shadow mode first</h3><p>This is the part I’d emphasise most to anyone building something similar.</p><p>Before we switched a single real payment, the full decision logic ran in <strong>monitoring-only mode</strong>. Every payment request was evaluated, and the system recorded what it <em>would have</em> done, without acting on it.</p><p>That surfaced things no test suite would have found:</p><ul><li>Banks whose conversion rate naturally fluctuates and would have caused false positives</li><li>Alternative mappings that were less equivalent than we had assumed</li><li>Edge cases in the decision logic that only real production traffic could produce</li></ul><p>Shadow mode is what gave us the confidence to turn switching on.</p><h3>Step 5: Make every decision observable</h3><p>Every decision, including the decision <em>not</em> to switch, emits a metric tagged with the bank, the primary vendor, the outcome and the reason. Actual switches are also written to a persistent log, tied to the payment, so any routing decision can be explained afterwards.</p><p>Every knob is a runtime option: switching on or off, monitoring-only mode, the window length, the drop threshold, the minimum sample size, and the per-bank baselines and alternatives. That means whoever is on call can:</p><ul><li>turn off switching instantly if something looks wrong</li><li>mark a bank unavailable before the signals catch up</li><li>tune thresholds for a bank with unusual traffic</li><li>fall back to monitoring-only mode to investigate</li></ul><p>All of this without a deployment.</p><p>This matters for two reasons. Practically, you can’t tune what you can’t see. Culturally, when a system makes autonomous decisions about real customer money, engineers need to trust it. You earn that trust by making the system auditable, not by promising it’s correct.</p><h3>Rollout</h3><p>We shipped in three phases, each behind runtime flags with a clear rollback path.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QaNYQ2_RZyrZjUepAz8_1w.png" /></figure><h3>What I’d do differently</h3><p><strong>Start collecting conversion data earlier.</strong> Baselines need real traffic to mean anything. I added the instrumentation during development, so we had less history than I’d have liked when it came to setting thresholds. Shipping the tracking first, on its own, would have given us confidence sooner.</p><p><strong>Take the bank mapping seriously from day one.</strong> I assumed it was a simple lookup table. It isn’t. “Same bank, different vendor” involves questions about account type coverage, market-specific behaviour, and differences in what each vendor supports. I underestimated this in the initial design and paid for it later.</p><h3>What’s next</h3><ul><li><strong>Adaptive baselines:</strong> derive each bank’s expected conversion rate from its own history, instead of configuring it by hand, so baselines stay accurate as bank behaviour changes.</li><li><strong>Broader coverage:</strong> grow the set of banks with alternatives, and apply the same pattern to other payment flows where similar redundancy exists.</li></ul><h3>Closing</h3><p>What stayed with me from this project is how much of the interesting engineering happens <em>around</em> the core logic. The “if unhealthy, switch” part fits on a napkin. Choosing signals that reflect customer reality, failing safe, validating in shadow mode, and making every decision auditable is what turns it into something you can trust.</p><p>Vendor degradations that previously required manual intervention are now handled automatically, helping prevent a negative customer experience.</p><p>And it went live before I’d finished my onboarding, which says as much about how Wise brings new engineers in as it does about the project.</p><p><em>Interested in problems like this? We’re always looking for engineers who want to work on real-money systems at scale.</em></p><p><a href="https://wise.jobs/jobs"><strong><em>Explore open roles at Wise</em></strong></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=876942d6031f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/wise-engineering/from-onboarding-to-production-how-i-built-dynamic-vendor-switching-at-wise-876942d6031f">From Onboarding to Production: How I Built Dynamic Vendor Switching at Wise</a> was originally published in <a href="https://medium.com/wise-engineering">Wise Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Observability At Scale — Every Byte Counts]]></title>
            <link>https://medium.com/wise-engineering/observability-at-scale-every-byte-counts-80f5e505d710?source=rss----f2565bbe9c46---4</link>
            <guid isPermaLink="false">https://medium.com/p/80f5e505d710</guid>
            <category><![CDATA[platform]]></category>
            <category><![CDATA[efficiency]]></category>
            <category><![CDATA[observability]]></category>
            <category><![CDATA[platform-engineering]]></category>
            <category><![CDATA[telemetry]]></category>
            <dc:creator><![CDATA[Milon James]]></dc:creator>
            <pubDate>Tue, 20 Oct 2026 13:03:15 GMT</pubDate>
            <atom:updated>2026-10-20T14:49:33.914Z</atom:updated>
            <content:encoded><![CDATA[<h3>Observability At Scale — Every Byte Counts</h3><p>How five changes cut our monthly observability spend by 40%. <em>Co-authored by</em> <a href="https://medium.com/u/008ddcc4c0d8">Andrew Marwood</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2aByfsAx6EWwz4e7KJKeJw.jpeg" /><figcaption>Photo by <a href="https://www.pexels.com/@hikaique/">Kaique Rocha</a> on <a href="https://www.pexels.com/photo/white-ceramic-tiles-108158/">Pexels</a></figcaption></figure><h3>Opening</h3><p>Wise’s mission is to build the best way to move and manage the world’s money with — <strong>minimum fees, maximum ease, full speed.</strong> That promise reaches around 19 million people and businesses across 160+ countries and processes around 4.7 million transactions a day. Wise has saved their customers <strong>£2.4 billion in hidden fees</strong> compared to banks over the past year [FY26].</p><p>Behind every transfer, balance check, and currency conversion is a foundational platform running over 1,000 microservices 24/7. That platform costs money to run, and those costs feed into the fees customers pay. To maintain service reliability, engineers depend on comprehensive observability for the components they own, making our telemetry stack one of the platform’s largest infrastructure consumers.</p><p>Platform efficiency is therefore a first-class KPI at Wise, alongside stability, productivity, risk and enablement. The Observability team’s mission is to <strong>empower teams to confidently, conveniently and efficiently visualise and act on system behaviours, ensuring the delivery of reliable products to our customers. </strong>A slow log query, a growing storage bill or a bottleneck in the logging pipeline may seem far removed from a customer’s transfer, but each affects the platform that Wise teams rely on to keep services dependable.</p><p>This article follows five investigations driven by a simple principle: <strong>efficiency is an ongoing engineering discipline, not a project with an end date</strong>. From changing the log compression algorithm and tuning query splits to upgrading the metrics collector and tracing a Fluent Bit connection limit, this article shows how removing waste and fine tuning configuration can support a reliable and responsive product. Some gains came from experiments, others from structural changes and deep forensic investigations. Together, they improved storage, network, and compute efficiency across our observability stack, resulting in a 40% reduction in our monthly spend.</p><h3>The Measurement Backbone</h3><p>The Cloud Economics team at Wise builds and maintains cost attribution tooling, providing dashboards with enough granularity to surface per-service, per-day spend, which enables teams to test hypotheses against actual spend. Without that visibility, most of what follows would have been invisible savings, or invisible waste. <strong>You can only optimise what you can measure</strong>. That principle applies equally to customer-facing features and to the platform beneath them.</p><h3>Storage: Challenging Our Compression Defaults</h3><p>Logs are often the first audit trail engineers reach for when investigating issues. When combined with other telemetry like metrics, traces, and profiles, they give teams a clear forensic view of a problem. The faster you can query these signals, the faster you can find the root cause; and the cheaper you can store them, the longer you can keep them.</p><p>At Wise, our observability team maintains an in-house Grafana Loki cluster that gives engineers access to real-time logs and historical logs. These logs were persisted in our production Loki S3 bucket at a scale of roughly 50TB per day and that number was constantly growing. Loki compresses chunks before storing them in S3. We had used the Snappy compression algorithm since the cluster’s early days. Grafana recommends Snappy as the default¹ for a reason: it’s fast, well-understood, strikes a solid balance between resource usage and compression, and enjoys broad support. For many workloads, it’s a sensible choice.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*V8kL1Mr1lOK9Jqs4-lC1Fw.png" /><figcaption>Loki S3 Bucket Size Growth (GB)</figcaption></figure><p>Given our rapid growth, it simply wasn’t the right fit anymore as was evident from the chart above, which maps out our Loki S3 bucket storage growth from Q4 2025 to Q1 2026. This prompted us to explore various optimisation opportunities across the Loki stack and the first question that sprang to mind was whether we were using the most efficient compression for the data persisted in the S3 bucket by the ingesters.</p><p>We looked at alternatives that might compress our logs more efficiently. The lz4 algorithm appeared promising, but we needed to make sure its storage savings would hold up at our scale without compromising performance. An open GitHub issue² from someone running Loki 2.9.2 on EKS caught our eye: their querier pods were OOMing during large range queries, with pprof traces pointing squarely at the lz4 block allocator (and they had to switch back to Snappy to fix it). Though that issue was from late 2023 and we were running a much newer Loki version (3.5.x), instead of making assumptions we wanted to validate whether we would run into the same issue. What made the experiment worth trying despite that warning was another detail buried in that same thread: lz4 was showing <strong>2 to 3x</strong> better compression ratios than Snappy for similar workloads.</p><p>We tested in a low-volume environment first, then staging, then production, with a clear rollback path at each step and the results were very promising. The irony of the whole optimisation was how little it actually took: unlocking these massive gains didn’t require an architectural overhaul, just a single line of configuration.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/652/1*yzUQdIjsahb_Fsjv1Fg6MA.png" /><figcaption>The one-liner change</figcaption></figure><p>In theory, trading compression speed for better space savings comes at a cost: ingesters have to burn more CPU cycles packaging chunks before flushing to S3, while queriers spend more compute decompressing them during log searches. However, in the first few days after our production rollout the metrics confirmed that lz4 delivered the <strong>40–50%</strong> storage drop with negligible impact on CPU/Mem across both the read and write path components.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*z9s65nZosjXqD3b8IY7mMg.png" /><figcaption>sum(rate(loki_ingester_chunk_stored_bytes_total[5m])) (GB)</figcaption></figure><p>Better yet, despite the warnings in that GitHub issue, we didn’t run into any negative performance impacts or query slowdowns, and our querier resource usage remained completely stable. Over the weeks that followed, roughly <strong>900TB </strong>worth of Snappy compressed<strong> </strong>chunks were removed from the production Loki bucket as the retention policies kicked in. This resulted in a sustained <strong>~50%</strong> reduction in overall S3 storage footprint.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*72rcdgraXPOgT-qtKoeMaw.png" /><figcaption>Loki S3 Bucket size after enabling lz4 compression (GB)</figcaption></figure><p>Cutting that storage footprint didn’t just shrink our AWS bill; it bought us operational runway. Cheaper storage means the team can justify longer retention, broader log coverage, or simply let the platform grow with Wise’s customer base without watching the bill scale linearly alongside it.</p><p>This single-line change not only delivered a massive boost in storage efficiency, but also unlocked unexpected savings across our cross-AZ (Availability Zone) footprint. Smaller chunks mean less data moving across zone boundaries on every write and read. Because lz4 shrank chunk sizes by 40–50%, it cut the volume of cross-AZ data traffic by that exact same proportion—translating to a significant direct daily cost reduction in cross-AZ network costs for our observability cluster. One config change, two cost levers pulled simultaneously.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cIq077EXLbsufPaJLhGCtQ.png" /><figcaption>sum(rate(container_network_transmit_bytes_total[5m])) (GB)</figcaption></figure><h4>Changing the compression algorithm used in OpenTelemetry collector</h4><p>The compression question extended to our tracing stack as well. The OpenTelemetry Collector is deployed into each of our Kubernetes clusters, and typically each collector in a deployment can process tens of thousands of spans per second. This gave us another opportunity to look at making a small change that would have a large impact at the scale we operate.</p><p>We noticed that gzip was enabled by default on our path from the OpenTelemetry Collector to Grafana Tempo, and realised there were optimisation opportunities based on what was inferred from the comparison of different compression algorithms detailed here³.</p><p>Switching the algorithm from gzip to zstd took us just eight hours to go from hypothesis to production. Because the pattern was already established, our measurement tooling was in place, and the team had the confidence to deploy quickly, we were able to validate the results immediately. We saw a roughly <strong>40%</strong> reduction in data received by our Tempo deployments.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1IPROzvrx5CQVfUQAPuIwg.png" /><figcaption>Sum Bytes Received by Tempo ALB</figcaption></figure><p>The compounding effect that we have described here comes from the combination of everything: the cumulative savings from each change, the insights we pick up along the way, and the velocity it gives us to apply those lessons to our next round of efficiency experiments. What we truly learned from these exercises is that defaults or recommendations are just a starting point, not a rule, and it should never stop one from testing what’s possible. At Wise we believe curiosity, a healthy dose of skepticism, and a willingness to experiment are what turn good engineering into great engineering.</p><h3>Protocols: The Cost of Delayed Upgrades</h3><p>Metrics tell Wise engineers whether their services are healthy in real time, allowing us to spot potential issues before our customers notice a problem. In our Kubernetes clusters we use Prometheus, managed via the kube-prometheus-stack, to scrape metrics and remote write them to Grafana Mimir which provides our long term storage and query layer. Every Prometheus instance was shipping metrics using Remote Write v1. It was reliable but verbose, due to the repetition of metric labels across payloads, and inefficiencies in payload encoding. Remote Write v2, finalised in 2024⁴, uses more efficient protobuf encoding and cuts the wire payload roughly in half.</p><p>The upgrade path to enable Remote Write v2 was not trivial. It required Prometheus Operator v0.76 to be upgraded to v0.78+, and kube-prometheus-stack to be upgraded to v66+, so the Prometheus Operator Custom Resource Definitions (CRDs) would support the messageVersion: V2.0 field for the remoteWrite configurations on our prometheusSpec definitions. These changes needed careful coordination to avoid potential compatibility issues.</p><p>Mimir needed no changes. It has supported Remote Write v2 since v2.17, and Wise runs v3.x. The automation pattern for cluster-wide rollouts was already established, which made a careful deployment across multiple Kubernetes clusters possible.</p><p>While Remote Write v1 worked, we avoided the complexity of the upgrade, but it turned out that avoiding this upgrade was resulting in much higher data transfer and as a result higher costs. That cost was real and recurring. We only saw it clearly once we measured it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LFU3QvERLu8RtPJez9vbAg.png" /><figcaption>Mimir Distributor Network Rx</figcaption></figure><p>Comparing the baseline metrics, traffic hitting Mimir distributors before and after the rollout across all clusters, the impact was clear: daily network transfers fell from <strong>~16TB per day to ~5.5TB per day</strong>, roughly a <strong>66%</strong> reduction. The size of this reduction significantly improved costs for cross xAZ traffic, load balancers, and TransitGateway traffic. The more efficient metrics pipeline also scales better. This lets the observability platform grow with Wise without the cost of visibility growing at the same rate.</p><p><strong>Compression as a first-class design decision</strong></p><blockquote><em>“</em>The art of programming is the art of organising complexity.<em>” — Edsger W. Dijkstra</em></blockquote><p>Wise processes petabytes of telemetry data every month. At this scale, compression isn’t just a configuration detail; it’s a critical lever for performance and cost. While default settings served us well when our stacks were first deployed, the possible savings became evident as the volume of telemetry data grew. Thinking about optimisation should not be triggered when a large bill arrives, but rather should be a continual practice as a platform grows and evolves.</p><p>The wins in logs (lz4 for Loki chunks), metrics (protobuf encoding in Prometheus Remote Write v2), and traces (zstd for the OpenTelemetry Collector) each followed the same pattern: identify a high-volume data path, challenge the inherited default, validate against your own workload, deploy carefully. Three different stacks, three different algorithms, one discipline. Every high-volume data path in an observability platform at scale is a candidate for this question. With the right measurement tooling, validating the answer takes hours, not quarters.</p><h3>Transit: Sending and processing less data</h3><p>Compression reduces bytes per payload. We also found opportunities to avoid moving or processing the same data unnecessarily.</p><h4>Choosing a Loki query split interval</h4><p>During incidents or other general debugging, a sluggish log query experience can slow an investigation which extends resolution times and deepens customer impact. Even though we had significantly scaled up the Loki chunks cache component with the intention of improving the user experience, it still fell short. To tackle the issue head-on, we needed to get a clearer picture of where the actual bottlenecks were.</p><p>Loki stores log data in compressed chunks, each covering a time span of ingested logs per stream. The time span covered by a chunk depends on ingestion rate — a high-volume stream fills a chunk in minutes, a low-volume stream may take hours. We found that our Loki query-splitting configuration was worth revisiting:split_queries_by_interval was set to 10 minutes, while production p90 chunk-bound duration (measures the time span covered by timestamps within a chunk) was around 50–60 minutes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rAD2SNIJeIVDmKBZ2neqxQ.png" /><figcaption>Loki p90 chunk-bound duration (hrs)</figcaption></figure><p>With split_queries_by_interval set to 10 minutes, , a query covering an hour is divided into six sub-queries. A chunk whose timestamps span, say, 10:03–10:58 overlaps all six windows. Each sub-query may need to consider the chunk and filter for its own time range, creating repeated work since the same chunk is retrieved and queried multiple times. Whether that means repeated storage reads depends on caching and the query path.</p><p>We set the split interval to one hour, close to the observed p90 chunk timestamp span of about 57 minutes and observed fewer redundant reads, lower read-path network utilisation, and a change visible in the xAZ cost dashboards.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vSFvv-B8X2AlUQmvdetAIQ.png" /><figcaption>Loki Querier Requests Count</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3_Rgm34927ifrqO6mpzo1Q.png" /><figcaption>Loki Querier Network Rx/Tx</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gvLgrP5twzGIlnvR3WKssQ.png" /><figcaption>Loki memcached Chunks Cache Network Rx/Tx</figcaption></figure><p>Queries now do fewer redundant chunk reads, which improves query performance and keeps costs from rising with query volume.</p><blockquote>Query splitting is one lever, but not the only one. We can also tune query parallelism and querier autoscaling to balance response time, resource use and cost. The observability team is also currently working on a data-quality initiative to standardise log fields around OpenTelemetry semantic conventions. Consistent fields should make queries easier to write and results easier to interpret, especially as more log investigations become agent-assisted. A shared schema gives agents clearer, more consistent signals to query, which can reduce ambiguity and help ground their answers. Combined with a leaner network traffic footprint, that should help teams retrieve useful results faster and shorten debugging and incident-resolution time.</blockquote><h3>An AI powered Investigation: When the Debugger Has the Full Picture</h3><p>Our logging pipeline involves Fluent Bit acting as an aggregator for writes to Loki. In our production Kubernetes cluster we contended with a bottleneck for some time without really understanding what the underlying issue could be.</p><p>This issue presented itself as TCP connection exhaustion in Fluent Bit which was expressed in logs as no upstream connections available. The only other symptom we had to work with was from the metric, node_netstat_Tcp_CurrEstab which increased according to the number of Fluent Bit pods running on the same node, but would plateau at around 2,100. Adding more pods to the same node resulted in more connection errors and ultimately dropped logs. To address the symptom we added topology constraints to prevent scheduling too many Fluent Bit pods on one node so that we didn’t hit the <strong>2,100</strong> supposed limit expressed by the node_netstat_Tcp_CurrEstab metric. This ensured a reliable flow of logs, but resulted in unnecessary waste in the Kubernetes cluster as more nodes were being scheduled to accommodate the topology spread constraints of Fluent Bit.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*U-h7dBte1jASk9N7TltzBA.png" /><figcaption>node_netstat_Tcp_CurrEstab (before)</figcaption></figure><p>We looked at this from various different angles, looking at TCP connection utilisation efficiency, EC2 instance network limits, and upstream and downstream constraints but were unable to pinpoint the exact cause until we brought in AI to help with the investigation.</p><p>The agent helped to structure the investigation with experiments at each stage to validate if the root cause was determined or not. At each stage the agent could validate the results through our agent integrations to our telemetry stack. The agents were also able to expand the investigation to look more broadly at other components and metrics that weren’t part of the initial scope.</p><p>AI wasn’t a miracle cure for the issue, a human still needed to guide the investigation and dig into the detail where required. But where the agent excelled in this instance was in its ability to collect and maintain more context about the issue, and to correlate disparate sources of information to build a full picture of the issue we faced.</p><p>We use Cilium for network security in our Kubernetes clusters, and ultimately it transpired that a Cilium Network Policy (CNP) was causing network traffic from Fluent Bit to follow a network path that we didn’t fully comprehend and we were hitting a limit within the embedded Envoy component of Cilium that was acting as a bottleneck on TCP connections.</p><p>With the issue understood we could adjust the CNP to remove the bottleneck, and from there we could remove the topology spread constraints for Fluent Bit which allowed our Kubernetes cluster to be scheduled more efficiently.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QBi008RHuE1IlDGfrYkCNA.png" /><figcaption>node_netstat_Tcp_CurrEstab (after)</figcaption></figure><p>Ultimately this reduced our scheduled node count by around <strong>15 r8g.16xlarge</strong> instances in our production observability Kubernetes cluster.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rMDxCG-5Ww5qX7sA2qOPLQ.png" /><figcaption>K8s cluster node count</figcaption></figure><p>This is what efficient observability infrastructure enables at its best: the humans, empowered with the tools to ask the right question, quickly, until the answer is clear enough to act on. Engineers debugging faster means customers experiencing fewer delays, fewer failed transactions, and fewer support interactions.</p><h3>Closing</h3><p>Together, lz4 for Loki storage, smarter query splitting, zstd for trace ingestion, node consolidation, and Remote Write v2 saved significant sums in annual network costs and compute plus hundreds of terabytes of storage. We achieved this without adding infrastructure or compromising reliability. The improvements stemmed from revisiting past challenges and scaling our solutions to match Wise’s growth.</p><p>The customer measure is on Wise’s mission page: over <strong>£2 billion saved each year in hidden fees</strong>. Platform efficiency directly contributes to that figure — the more affordable the platform, the more we can reinvest in shaping the product our customers truly want. Every dollar we avoid spending on redundant network traffic, half-empty nodes, or oversized storage can support the mission of enabling customers to move their money in a cheaper, faster and more transparent way.</p><p>These changes also give product engineering teams a leaner observability stack. They can rely on it to debug faster, understand their services more deeply, and build with confidence. The Platform mission is to build foundations that help teams ship confidently and efficiently. The Observability team’s role is to help those teams see clearly and act quickly.</p><p>Compression algorithms, query intervals, scheduler configurations, and protocol versions are ordinary technical choices, but at scale they affect how Wise keeps its promise. As AI-assisted debugging becomes more common, these improvements give the agents a more reliable, responsive observability stack to work with, helping engineers get useful results faster and respond to incidents sooner.</p><p><strong>Efficiency, at Wise, is not a constraint. It’s a feature.</strong></p><p>Want to know more? Read our engineering stories on <a href="https://medium.com/wise-engineering">Medium</a>, or check out our <a href="https://bit.ly/4k6wIPN">open roles</a> to help shape the future of our products.</p><ol><li><a href="https://grafana.com/docs/loki/latest/configure/bp-configure/#consider-the-snappy-compression-algorithm">https://grafana.com/docs/loki/latest/configure/bp-configure/#consider-the-snappy-compression-algorithm</a></li><li><a href="https://github.com/grafana/loki/issues/11247">https://github.com/grafana/loki/issues/11247</a></li><li><a href="https://github.com/open-telemetry/opentelemetry-collector/blob/main/config/configgrpc/README.md#compression-comparison">https://github.com/open-telemetry/opentelemetry-collector/blob/main/config/configgrpc/README.md#compression-comparison</a></li><li><a href="https://prometheus.io/docs/specs/prw/remote_write_spec_2_0/">https://prometheus.io/docs/specs/prw/remote_write_spec_2_0/</a></li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=80f5e505d710" width="1" height="1" alt=""><hr><p><a href="https://medium.com/wise-engineering/observability-at-scale-every-byte-counts-80f5e505d710">Observability At Scale — Every Byte Counts</a> was originally published in <a href="https://medium.com/wise-engineering">Wise Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Data Inventory]]></title>
            <link>https://medium.com/wise-engineering/data-inventory-4eff3f015553?source=rss----f2565bbe9c46---4</link>
            <guid isPermaLink="false">https://medium.com/p/4eff3f015553</guid>
            <category><![CDATA[frontend]]></category>
            <category><![CDATA[security]]></category>
            <category><![CDATA[platform]]></category>
            <category><![CDATA[data-governance]]></category>
            <category><![CDATA[backend]]></category>
            <dc:creator><![CDATA[Ritesh Modi]]></dc:creator>
            <pubDate>Wed, 19 Feb 2025 14:24:14 GMT</pubDate>
            <atom:updated>2025-02-19T14:24:14.111Z</atom:updated>
            <content:encoded><![CDATA[<p>The Data Governance team, part of Data Platform, develops and operates platform products through which autonomous teams can govern their data. The team’s vision is to democratise data in a secure and compliant manner. Data Inventory was the team’s first product.</p><h3>What is Data Inventory?</h3><p>A data inventory is an extensive catalog of the Wise’s data assets. It helps us understand where the data is located and what kind of security / privacy controls are in place. This is also a requirement for<a href="https://www.isms.online/iso-27001/annex-a-8-asset-management/"> ISO 27001:2022</a>.</p><h3>What are the constraints?</h3><p>Any system has to work well with our controls and way-of-working. This led to following constraints:</p><ul><li>It shall be built in a way that satisfies our security controls such as network segmentation, least privilege access, secure secret management, etc.</li><li>It shall be easy to integrate with other in-house platforms to provide cohesive experience.</li><li>It shall be built to connect with heterogeneous and fragmented infrastructure. Many data systems are built through off-the-shelf or managed service offering but others are built &amp; operated in-house.</li><li>The system shall be integrated by default with technology provided by Data Platform. On another hand, it shall be extensible enough to integrate with data systems not maintained by Data Platform.</li></ul><h3>How did we build our inventory?</h3><p>At high level, we split this into 4 main outcomes:</p><p>1. Register assets at correct granularity.<br> 2. Identify the correct owner and make them accountable for the life cycle.<br> 3. Extract the schema of the data asset.<br>4. Classify the asset with correct sensitivity.</p><p>This is how design looks at high level:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*MHu101Yv9CGNGIGR" /></figure><p>To achieve the above design and outcome, we follow the following steps:</p><h4>Step 0: Discover data systems network metadata</h4><p>Scanners need to be aware of the network metadata before doing scanning. For example:</p><p>• What is the host or connection string?<br> • What is the technology of the data system?<br> • What is the name of the data system?</p><p>Let us take an example of how we solve it in our operational databases.</p><p>We follow a database-per-service pattern. Operational databases are provisioned by the Database Platform team (DBP) in a semi-automated manner i.e. 1/ product teams request the database through a self-service user interface 2/ DBP apply the terraform plan and run provisioning script to create machine users.</p><p>We built a system to discover the databases as and when they are created or deleted.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*A1FrlFrjBVevi6Xx" /></figure><p>This enabled data inventory to have all the metadata for scanning the operational databases.</p><h4>Step 1: Register assets at the correct granularity</h4><p>Once we have discovered the data systems then the next step is to enumerate the assets and register them at the central repository.</p><p>Registering the asset in the inventory has to follow 3 main properties:</p><p>1. asset_id shall be self explanatory or human readable.<br> 2. asset_id shall have the right granularity to capture sufficient details. <br> 3. asset_id shall be unique to validate the security and compliance controls.</p><p>Format of the asset identifiers</p><pre>data_asset://{asset_type}/{asset_sub_type}/{top_level_identifier}/{sub_level_identifier}<br><br>where:<br>    asset_type = {MariaDB, PostgreSQL, S3, Kafka}<br>    top_level_identifier = {database_name, topic_name, bucket_name}<br>    sub_level_identifier = {table_name}<br><br>Example:<br>    1/ data_asset://mariadb/payout/anomaly<br>    2/ data_asset://s3/data_inventory<br>    3/ data_asset://postgresql/db_bank/public/bank_account</pre><p>Let us take an example of payout database with following asset_id:</p><pre>asset_id = &quot;data_asset://mariadb/payout/anomaly&quot;<br>asset_id = <br>  data_asset://{asset_type}/{top_level_identifier}/{sub_level_identifier}<br><br>where<br>    asset_type = mariadb<br>    top_level_identifier = payout<br>    sub_level_identifier = anomaly (table_name)</pre><p>Granularity of the asset identifiers</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*3e0TLUv0ddS-epDQ" /></figure><ul><li>Lesser the sensitivity, higher the granularity (capture less detail)</li><li>Higher the sensitivity, lower the granularity (capture more detail)</li></ul><h4>Step 2: Identify the correct owner of data and make them accountable</h4><p>Data ownership is important for the following reasons:</p><p>• Data Owners are aware of the content of the data system and surrounding business context. Hence they can classify the data based on sensitivity and criticality.<br> • They determine the authorised use, govern access controls, and ensure compliance with applicable laws, regulations, and company policies.<br> • Owners are the point of contact for cost, management, and lifecycle.</p><p>In 90% of the use cases, we can do the ownership attribution through additional metadata provided by product teams. For remaining cases, we came up with rough algorithm:</p><p>• Producer: Any service writing the data gets ownership by default.<br> • Consumer: Any service reading the data gets ownership next (this case can be triggered if producer has stopped writing)<br> • No consumer and producer: Last producer or consumer needs to drive the deletion of the data system.</p><p>Even though the above process has served us well so far, there are still unicorns and we try to keep them under 1% of the population.</p><h4>Step 3: Extract the schema of the data assets</h4><p>Schema of the data systems is important to build more granular controls like understanding the data, what columns might contain sensitive data, what data may be the candidate for data deletion, etc.</p><p>Extraction of schema for relational data systems is easy. We used information schema for MariaDB and <a href="https://www.postgresql.org/docs/current/catalogs.html">system catalogs</a> for PostgreSQL. We still need to solve the schema understanding for non-relational data systems in a secure manner.</p><h4>Step 4: Classify the asset with correct sensitivity</h4><p>As a first iteration, we built a simple user interface for teams to classify their data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*CtwyJ4UOeNZLOyUG" /></figure><p>Benefit of the user interface approach:<br> • We can build one product for all different kinds of data systems and it scales well in comparison to engineering development cost.</p><p>Drawback of the approach:<br> • It does not provide good developer experience as the team has to step out from their development flow.</p><p>We have plans to improve the data classification approach which is less disruptive for developers.</p><h4>Step 5: Notify teams to classify their data assets</h4><p>In Wise, we have multiple mechanisms to notify engineers about mandatory campaigns or programs. We leveraged 3 layers of notifications: 1/ Soft nudges 2/ Intrusive notifications in Pull Requests 3/ Direct call to action.</p><p>We still need to go a long way to notify the teams in an optimal way which does not lead to bad developer experience.</p><h3>What are the learnings?</h3><ul><li>Qualitative feedback is much more economical in platform products than consumer products. Leverage direct feedback or closer proximity with your users.</li><li>When the problem is ambiguous, it is better to solve the problem for 80% of the cases with a two-way door decision and make some practical progress than get stuck in an analysis-paralysis loop.</li><li>Platform products are complex, hence can take a couple of quarters. Leverage stepping stones that deliver concrete value and illuminate “unknown unknowns”.</li><li>We could not use other data catalogs like DataHub, OpenMetadata or BigID as they require lax security controls which does not work with our operating model.</li></ul><p>We will be expanding the team to build many interesting products. Join us on this exciting adventure, and be part of an innovative and impact driven team. Check out <a href="https://wise.jobs/jobs?options=33,343&amp;page=1">our open Engineering roles here</a> if you wish to join our team!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4eff3f015553" width="1" height="1" alt=""><hr><p><a href="https://medium.com/wise-engineering/data-inventory-4eff3f015553">Data Inventory</a> was originally published in <a href="https://medium.com/wise-engineering">Wise Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Wise Tech Stack (2025 update)]]></title>
            <link>https://medium.com/wise-engineering/wise-tech-stack-2025-update-d0e63fe718c7?source=rss----f2565bbe9c46---4</link>
            <guid isPermaLink="false">https://medium.com/p/d0e63fe718c7</guid>
            <category><![CDATA[platform]]></category>
            <category><![CDATA[data]]></category>
            <category><![CDATA[engineering]]></category>
            <dc:creator><![CDATA[Milon James]]></dc:creator>
            <pubDate>Wed, 12 Feb 2025 13:34:08 GMT</pubDate>
            <atom:updated>2025-06-05T09:54:59.309Z</atom:updated>
            <content:encoded><![CDATA[<h3>Wise Engineering</h3><p>As of the financial year 2025, Wise supports <strong>15.6 million</strong> active customers, moving approximately £36 billion across borders each quarter. Approximately <strong>65%</strong> of our transfers arrive instantly, and our <a href="https://platform.wise.com/">Wise Platform</a> facilitates payments for banks, financial institutions and enterprises globally. This success is driven by our technology-first approach, robust architecture, and dedicated engineering teams.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ATsu7QSH8hn5OkgrDbv3ow.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/@randyfath">Randy Fath</a> on <a href="https://unsplash.com/photos/people-building-structure-during-daytime-ymf4_9Y9S_A">Unsplash</a></figcaption></figure><h4>How we work at Wise</h4><p>Wise has more than 850 engineers working across key global locations, organised into independent squads and tribes. These teams are empowered to innovate and make decisions independently, fostering transparency, trust, and collaboration.</p><p><em>This article follows our </em><a href="https://medium.com/wise-engineering/wise-tech-stack-2022-edition-a6ac089a382f"><em>Tech Stack 2022</em></a><em> to cover the most recent improvements in Wise’s tech stack that enable us to achieve our mission of money without borders — instant, convenient, transparent and eventually free.</em></p><h3>Moving money with Wise</h3><h4>Our web and mobile applications</h4><p>Our web applications are built using CRAB (a Wise-specific abstraction on top of the popular Next.js framework) and comprises of 40 distinct apps, each handling specific product functions making deployments safer and more manageable.</p><p>One of the biggest changes has been in our testing methodology. We’ve adopted <a href="https://storybook.js.org/">Storybook</a> for visualising individual React components during development. Storybook pairs really well with <a href="https://www.chromatic.com/">Chromatic</a> which captures snapshots after each change and can highlight the visual differences in the component. These snapshots are very effective for catching visual regressions during code changes which helps us prevent bugs from reaching our customers.</p><p><strong>Wise mobile app: faster, smarter, and more efficient</strong></p><p>Our <strong>iOS engineers</strong> have upgraded our infrastructure by migrating 250+ Xcode modules from Xcodegen to Tuist and switching from Cocoapods to Swift Package Manager (SPM), unlocking improvements in caching for builds. The team also improved flexibility, reducing zero-change <strong>build times from 28s to 2s</strong>. Development is smoother than ever now with the advanced build caching.</p><p>Our <strong>Android engineers</strong> remain focused on developing apps at scale. Our primary Android repository contains over 300 Gradle modules and roughly 1 million lines of code, comprising 2 production apps, 6 sample apps, 17 JVM modules, 221 Android modules, and 65 multiplatform modules. Our efforts to improve Android development velocity focus on these key themes:</p><ul><li>Using more <a href="https://blog.bitsrc.io/bff-pattern-backend-for-frontend-an-introduction-e4fa965128bf">BFFs</a> to share code logic between Android, iOS, and web teams.</li><li>Development of code generation tools built on <a href="https://kotlinlang.org/docs/ksp-overview.html">KSP</a>.</li><li>Exploring applications of <a href="https://kotlinlang.org/docs/multiplatform.html">Kotlin Multiplatform</a>.</li></ul><p>On the user interface side, we’ve completely moved to Compose — first for our design system, and now for entire screens and navigation. We stay current with technology, quickly adopting Kotlin 2.0 and 2.1 when they were released. For handling asynchronous tasks we use Coroutines and Flows, while our architecture adheres to the standard MVVM patterns and is supported by Google’s Jetpack libraries.</p><h4>Backend services</h4><p>Wise runs on over <strong>1000 services in total</strong>. In the backend we are primarily using Java and Kotlin. Since our last update, we have focused on automation and efficiency by developing in-house tools that improve development velocity and provide standard libraries to use across different services.</p><p><strong>Building great applications faster</strong></p><p>Since our last update, we have been focusing on <strong>enabling engineering at scale with automated code updates and scalable dependency management solutions</strong>. For this, we have:</p><ul><li>Introduced an in-house microservice chassis framework, built on the principle of minimal configuration and shipped as an artifact, allowing us to build standard micro services faster. It configures common capabilities that services use with a recommended defaults: security, observability, database communication, working with Kafka, and more, allowing teams to focus on business logic.</li><li>Improved standardisation of build pipelines through an in-house collection of Gradle plugins. A notable example is our plugin that standardises GitHub Actions workflows. This enables organisation-wide workflow changes through simple plugin version updates, making initiatives like SLSA rollout effortless across our 700+ Java repositories.</li><li>Introduced a language-agnostic automation service that enables us to make complex changes to the codebase at scale and create pull requests for the owning team to review. Using this service, we took our centralised Java dependency management platform a step further by automating dependency upgrades for Java services.</li></ul><h4>Directly integrating with local payment schemes</h4><p>We went live with InstaPay, the instant payments system in the Philippines and were granted access to join Zengin, Japan’s instant payments system. We also received access to PIX in Brazil.</p><p>At Wise we put substantial effort into creating the most consistent architecture possible, with networking centralised using <a href="https://aws.amazon.com/transit-gateway/">AWS Transit Gateways</a>. There is substantial variance between the details of the integrations with physical data centres required in the UK, Hungary and Australia. Our Australian data centres were one of the first deployments of <a href="https://aws.amazon.com/outposts/servers/features/">AWS Outpost Servers</a>, allowing us to maintain consistent AWS tooling across as much of our infrastructure as possible.</p><h4>Allowing businesses to use our API</h4><p>Our public API allows businesses to integrate Wise’s cross-border payment services directly, using secure <strong>REST APIs backed by OAuth authentication</strong>. This provides <a href="https://wise.com/gb/business/api">businesses</a> with functionality for transfers, currency exchange, and account management, along with comprehensive documentation and developer tools to streamline the integration process.</p><p><a href="https://platform.wise.com/">Wise Platform</a> supports over <strong>40</strong> <strong>currencies</strong> and multiple payment routes, delivering seamless, globally connected solutions. The platform includes built-in compliance features, allowing for seamless cross-border operations while tapping into Wise’s extensive global infrastructure.</p><h3>Scaling Wise’s infrastructure platform</h3><p>To accommodate rapid growth, we’ve focused on rebuilding our infrastructure to ensure efficiency and flexibility while reducing operational burdens on our teams.</p><h4><strong>Introducing our new Kubernetes-backed Compute Runtime Platform</strong></h4><p>The <strong>Compute Runtime Platform (CRP)</strong> is our new, scalable platform leveraging Kubernetes, allowing engineering teams to host applications effortlessly without managing complex infrastructure setups.</p><p><strong>Evolving our Kubernetes stack</strong></p><p>Since 2018, Wise has relied on Kubernetes<strong> </strong>built with Terraform, JSONNET, and ConcourseCI to support service-mesh controls (Envoy), PCI-DSS compliance, and frictionless deployments. While this model served us well, we needed a more scalable &amp; standardised approach. This is why we introduced CRP:</p><ul><li>Terraform still provisions infrastructure, but we rewrote our codebase from scratch for flexibility and maintainability.</li><li>RKE2 handles cluster bootstrapping, with Rancher managing overall cluster state.</li><li>Helm replaces JSONNET for better maintainability and upstream compatibility.</li><li>ArgoCD with custom plugins ensures fully automated provisioning &amp; consistency.</li><li>Our Envoy-powered service proxy now includes seamless integration &amp; discovery between services, boosting flexibility, resilience, and oversight across our platform.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FiGHm58o_T1Cyujym01S6g.png" /></figure><p>As a result, we’ve <strong>grown from 6 Kubernetes clusters to more than 20</strong> while keeping maintenance manageable and efficient.</p><p><strong>Smarter autoscaling &amp; cost optimisation</strong></p><p>Alongside the ability to better provision and maintain our infrastructure, we have also introduced efficiency improvements with CRP:</p><ul><li>We’re building a flexible, opt-in autoscaling solution to reduce cloud costs &amp; cognitive load for teams.</li><li>Automated container CPU <a href="https://aws.amazon.com/blogs/opensource/right-size-your-kubernetes-applications-using-open-source-goldilocks-for-cost-optimization/">rightsizing</a> (via Vertical Pod Autoscaler) is now live in non-production and rolling out to production for non-critical workloads.</li><li>Fully managed sidecar containers (like Envoy proxy) now simplify deployments for product teams.</li><li>Expanding horizontal scaling with <a href="https://keda.sh/">KEDA</a>, optimising workloads based on daily &amp; weekly traffic patterns.</li></ul><p>The focus on cost optimisation is helping Wise move closer to <a href="https://wise.com/p/our-mission"><strong>Mission Zero</strong></a>.</p><h3>Building a scalable, reliable, and intelligent data infrastructure</h3><p>A lot of what we do at Wise comes down to moving and making sense of data. Whether it’s transferring funds, updating real-time dashboards, or powering machine learning models behind the scenes, our systems are constantly processing and distributing huge volumes of information. As our global footprint grows, so does our need for faster, more secure, and more flexible ways of handling data. Below is a quick look at how we’re evolving our data tech stack to keep delivering reliable, convenient, and efficient experiences for our customers.</p><h4><strong>Powering Our Data Backbone</strong></h4><p>At Wise, our databases are one of the foundations of everything we do, so we’ve invested a lot in making them both robust and easy to manage. Behind the scenes, our database engineers are tackling fascinating technical challenges that push the boundaries of what’s possible in financial data management.</p><ul><li>We’ve worked hard to migrate most of our MariaDB and Postgres workloads off EC2 and into Amazon RDS. This shift has cut down on maintenance tasks, reduced operational overhead, and offered more robust security features.</li><li>Likewise, we’re moving from self-hosted MongoDB to MongoDB Atlas, which frees us up to focus on building new features rather than wrestling with scaling.</li><li>Redis continues to power our in-memory workloads.</li><li>We’re also currently exploring distributed databases for greater relational scalability.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3bHF_ughnPcJi1epuqewLw.png" /></figure><h4><strong>Smarter workflow orchestration &amp; observability</strong></h4><ul><li>We have adopted a workflow engine called <a href="https://temporal.io/">Temporal</a> to automate critical tasks like <a href="https://medium.com/wise-engineering/how-wise-reduced-aws-rds-maintenance-downtimes-from-10-minutes-to-100-milliseconds-18ab69fe2346">switchovers</a> and recovery tests. This helps us keep downtime to a minimum and stay compliant with strict regulations on resilience.</li><li>Tools like RDS Performance Insights and Percona Monitoring and Management (PMM) give us a clear view of how our databases are doing, so we can tackle issues early.</li><li>We’re also experimenting with using direct cloud SDKs to manage our infrastructure — moving away from Terraform Enterprise to simplify our provisioning processes.</li></ul><h4><strong>Keeping Data in Motion</strong></h4><ul><li>Kafka underpins most of our real-time data movement — whether it’s asynchronous messaging between services, log collection, or streaming updates for analytics.</li><li>Our Kafka clusters have grown significantly in capacity, and we’ve introduced features like rack-aware stand-by replicas to boost fault tolerance.</li><li>Our in-house data movement service helps funnel information from Kafka or databases into destinations like Snowflake, S3 Parquet, Iceberg, or other targets.</li><li>Automated checks in the configuration process reduce human error, and its growing usage shows teams are finding it simpler and faster to set up new pipelines.</li><li>Another in-house service, Data Archives, now archives more than 100 billion records across multiple databases. This not only saves on costs but also makes our databases easier to back up and recover.</li></ul><h4><strong>Turning Data into Insights</strong></h4><p>Teams across Wise use our Business Intelligence tools to make strategic, data-driven decisions that enhance the customer experience — from fraud detection to personalised marketing and predictive analytics.</p><ul><li>Although we still rely on Snowflake as a core component of our analytics, we’ve been laying the foundations of a Data Lake on Amazon S3 using Apache Iceberg. Thanks to its robust open table format, Apache Iceberg enables us to store huge amounts of data on S3 more efficiently. It lets us modify our table structures without needing to rewrite all the data, which helps our queries run faster and keeps storage costs in check. Plus, its active open source community continuously drives improvements that benefit our long-term scalability.</li><li>Sitting between our data sources and business intelligence tools is Trino, which lets us query Iceberg tables, Snowflake, or even Kafka streams in one place.</li><li>A new Trino gateway handles workload separation and fault-tolerant queries, while complex workflows continue to be managed by Airflow and dbt-core. For an in-depth look at this topic, watch our data engineers’ recent conference <a href="https://www.youtube.com/watch?v=K5RmYtbeXAc">presentation</a>.</li><li>Reporting and dashboards are built with Looker or Superset — teams choose whichever toolset fits best.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ebFu8iFjqXGSzvy1fAayvA.png" /></figure><h4><strong>Driving Intelligent Solutions</strong></h4><p>Our machine learning architecture is designed to support both exploration and production, seamlessly integrating ML features into products to improve customer onboarding and fraud prevention, and leveraging responsible AI tech.</p><ul><li>Our data scientists work in Amazon SageMaker Studio, choosing either JupyterLab or VSCode to build experiments and explore data.</li><li>Large-scale processing happens on Spark in EMR, while Airflow orchestrates data collection, cleaning, model training, and periodic re-training to keep every step on schedule.</li><li>We use SageMaker Feature Store to keep hundreds of features in sync for both training and inference, and MLflow tracks experiments, metrics, and model versions. This setup simplifies comparing model variants or rolling back if needed.</li><li>When a model is ready for production, we deploy it through an in-house prediction service based on Ray Serve.</li><li>Thanks to MLflow plugins, our data scientists can roll out models with minimal friction — speeding up inference times for fraud detection, KYC, or other use cases where every millisecond counts.</li><li>Automated checks help catch data drift or feature inconsistencies before they turn into serious issues.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Rux3bEKC2spJhQ3H6sOr-g.png" /></figure><h4><strong>Unlocking New AI Capabilities</strong></h4><p>We’ve created a secure gateway that connects us to multiple large language model providers, including Anthropic (Claude), AWS (Bedrock), Google (Gemini), and OpenAI (gpt and o families). This approach lets us experiment with different models without juggling separate credentials or complex compliance checks. A Python library, inspired by LangChain, wraps these APIs to speed up prototyping.</p><p>For cases where we need to reference internal documents, knowledge bases, or user data, we offer a custom Retrieval-Augmented Generation (RAG) service. It pulls the latest information from various data stores before generating responses — a handy feature for summarising complex documents or automating parts of customer service workflows.</p><h4><strong>Smart Data Management</strong></h4><p>Our data architecture is both vast and complex, so we’ve built a comprehensive inventory system and a dedicated governance portal to show where data is stored and how it’s classified.</p><p>We have automated data discovery across our data estate to know what data is created; who has created it; and what is the category of the data. We are leveraging our inventory of data in data deletion, data compliance and data discovery initiatives. This setup not only supports compliance efforts for audits and regulations but also boosts developer productivity.</p><p>With more engineers joining the governance effort, we’re able to roll out stricter policies, enhanced privacy checks, and automated data lifecycle management across the board.</p><h3>Developer Enablement — Evolving CI/CD at Wise</h3><p>To strengthen our delivery pipeline and developer experience, we’re continuously evolving our CI/CD platform to empower developers to ship features to customers faster and more reliably than ever before.</p><h4><strong>CI Improvements: speed and security</strong></h4><p>The migration from CircleCI to Github Actions brought new possibilities for optimisation. By implementing detailed metrics tracking, we uncovered crucial insights into build performance. For example, by pre-populating caches for frequently used containers, we<strong> slashed build times by 15%</strong>. <strong>At our scale of 500K monthly builds, this translates to over 1,000 hours saved each month.</strong></p><p>We’ve been methodically implementing the <a href="https://slsa.dev/spec/v1.1/about">SLSA framework</a> across our build processes, strengthening our supply-chain security one language at a time.</p><h4><strong>CD Transformation from Octopus to Spinnaker</strong></h4><p>Following up on our earlier <a href="https://medium.com/wise-engineering/state-of-our-ci-cd-pipeline-part-2-29bd17515e6f">post</a> about the state of our CI/CD pipeline, our deployment strategy has shifted with the transition from Octopus, our in-house tool to <a href="https://spinnaker.io/">Spinnaker</a>. This wasn’t just a tool swap — it represented a paradigm shift from viewing deployments as simple transactions to seeing them as orchestrated sequences of events.</p><p>This transformation allowed us to reduce engineering time spent on deployment management, and to minimise the risk of defects reaching customers. This has increased developer velocity to deliver to our customers much faster, without sacrificing quality and stability.</p><h4><strong>Advanced canary testing</strong></h4><p>Spinnaker’s <a href="https://spinnaker.io/docs/guides/user/canary/">Automated Canary Analysis</a> has become a cornerstone of our deployment pipeline. The process is elegant in its simplicity yet powerful in execution:</p><ul><li>Only 5% of traffic routes to new service versions during testing</li><li>Comprehensive 30-minute analysis of technical and business metrics</li><li>Automatic rollback triggers for significant anomalies</li></ul><p>As a result, in 2024 alone this system automatically prevented hundreds of potentially incident-causing deployments and saved thousands of engineering hours.</p><p>With over half of Wise’s services now running on Spinnaker and full migration expected by mid-2025, we’re positioned to take the next step: implementing managed delivery to orchestrate the entire SDLC, including testing and data management.</p><h3>LGTM stack for observability</h3><p>We have improved our observability ecosystem to better monitor, understand, and optimise the Wise product. The reliability engineers are focused on building a more powerful, efficient, and insightful observability platform that addresses critical challenges in our rapidly scaling environment.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eH9-LcxB8IGstQdZ9N8Spg.png" /></figure><h4><strong>Dedicated observability infrastructure</strong></h4><p>We have implemented dedicated observability CRP clusters. This provides out-of-the-box observability for services running across different environments. As a result, we have simplified the monitoring setup and reduced manual configuration overhead.</p><h4><strong>Unified metrics and monitoring stack</strong></h4><p>To address scalability we have moved over from Thanos to Grafana Mimir. This means that we are now running fully on the <a href="https://grafana.com/go/webinar/getting-started-with-grafana-lgtm-stack/">LGTM stack</a>: Loki for logs, Grafana for dashboards and visualisation, Tempo for traces and Mimir for metrics. As part of our continuous improvement in observability, we’re pilot testing Grafana Pyroscope for profiling select services, exploring new dimensions of performance insight and optimisation.</p><p>Our metrics stack is ingesting ~6 million metric samples per second and processing 150 million active series for our largest metric tenant.</p><p>By unifying our stack, we have:</p><ul><li>Standardised observability across our entire technology ecosystem.</li><li>Enhanced correlation between logs, metrics, traces, and dashboards.</li><li>Improved performance and scalability of our monitoring infrastructure.</li></ul><h4><strong>Cost optimsation and efficiency in observability</strong></h4><p>Lastly we have continued to invest in optimising our observability stack. We have been able to reduce operational costs, improve resource utilisation and ultimately have a more sustainable long-term observability strategy. Check out our previous <a href="https://medium.com/wise-engineering/grafana-mimir-compaction-from-bottleneck-to-savings-b26c6b0125a6">article</a> that details some of the work that we did on these initiatives.</p><p>This strategic evolution empowers our engineering teams with deeper, more actionable insights while ensuring our observability infrastructure remains both powerful and cost-effective.</p><h3>Conclusion</h3><p>In wrapping up, our 2025 tech stack is a testament to how we’re steering the ship at Wise to provide the fastest, most reliable, and cost-effective way to move money for our <strong>15.6 million active customers worldwide</strong>. A big focus on standardisation and integration means our systems are built to scale efficiently while ensuring robust risk and compliance management.</p><p>Our engineering teams continue to refine our infrastructure across all areas, from mobile and web applications to backend services and machine learning. These efforts simplify and accelerate how money moves across borders, ensuring we’re ready for both current demands and future growth.</p><p><strong>We are committed to long-term investments in building the best infrastructure to move and manage the world’s money</strong>. With each technical enhancement and new direct connection to payment systems, we’re steadily progressing towards our vision of achieving <strong>Money Without Borders</strong>.</p><p>If you are curious in what we do, check our tech blog on <a href="https://medium.com/wise-engineering">Medium</a>, and <a href="https://www.wise.jobs/engineering?utm_source=engineeringblog&amp;utm_medium=blog&amp;utm_campaign=careers_site">check out our open Engineering roles here</a> if you wish to join our team!</p><p>I’d like to thank my colleagues who contributed to both the work described in this blog and the writing process itself. Special thanks to my team members and team lead Marta Lima, for the encouragement and for their editorial support; Sten Raudmets for the help with Frontend section; Forrest Pangborn and Dmitry Serov for mobile; Pavel Dionisev for his help on the Backend section; Ed Hargin for sharing details about the Platform Integrations; Adrian Lopez for the content and editorial support on the Data &amp; AI/ML section; Ritesh Modi, Geno Racklin Asher<strong> </strong>and Telmo Oliveira for the help with the Data Governance &amp; Analytics content; James Bach-Nutman for his content contribution on the Compute platform and Kubernetes; Ervin Lumberg and Doron Solomon for the updates on the CI/CD stack; Massimo Pacher for the overall encouragement and guidance; Zsofia Kiss for the assistance on employer branding and every other person who reviewed the draft and shared their feedback. Your insights and time made this blog post possible!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d0e63fe718c7" width="1" height="1" alt=""><hr><p><a href="https://medium.com/wise-engineering/wise-tech-stack-2025-update-d0e63fe718c7">Wise Tech Stack (2025 update)</a> was originally published in <a href="https://medium.com/wise-engineering">Wise Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How did we build 3D animations in mobile apps?]]></title>
            <link>https://medium.com/wise-engineering/how-did-we-build-3d-animations-in-mobile-apps-515c3de87b74?source=rss----f2565bbe9c46---4</link>
            <guid isPermaLink="false">https://medium.com/p/515c3de87b74</guid>
            <category><![CDATA[3d]]></category>
            <category><![CDATA[optimization]]></category>
            <category><![CDATA[mobile]]></category>
            <category><![CDATA[animation]]></category>
            <dc:creator><![CDATA[Yağız Gürgül]]></dc:creator>
            <pubDate>Wed, 29 Jan 2025 16:59:35 GMT</pubDate>
            <atom:updated>2025-01-29T16:59:35.879Z</atom:updated>
            <content:encoded><![CDATA[<p>In March 2023, Wise launched its new brand — as part of it, we introduced a collection of bold new 3D illustrations. While visually stunning, our brand team had bigger ambitions to bring them to life and elevate Wise further.</p><p>This took us on a journey to understand the technical landscape and challenges around creating a seamless multi-platform solution for interactive 3D animations.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/480/1*CGiHGpAGZia78b-GYZ9_1A.gif" /><figcaption>interactive 3D intro on Wise iOS app</figcaption></figure><h3>The new illustration set in Wise</h3><p>Our new illustration set took inspiration from icons, allowing us to communicate with a global audience. They’re unique, bold and dynamic, even when still.</p><p>They give us opportunities to share moments of delight with our customers and help our brand stand out from billboard to button.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AMTbTN-IAEwT8yIRM6miWg.png" /><figcaption>check out <a href="https://wise.design/">https://wise.design/</a> for more!</figcaption></figure><p>What sets illustrations at Wise apart is that they’re not static. Each one is paired with a crafted 3D model, featuring detailed UV maps, precise shading, and carefully designed lighting.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*cx6Vrhv5Jk_cS1eb8MXrww.gif" /></figure><p><strong>This raises an exciting question: What if we rendered these illustrations dynamically in real time, making them interactive on mobile apps?</strong></p><h3>Native or web view?</h3><p>To render illustrations in real time, we explored several technical solutions:</p><ul><li>Native</li><li><a href="https://github.com/google/filament">Filament</a></li><li><a href="https://threejs.org">three.js</a></li></ul><p>We opted to create 3D scenes using <a href="https://threejs.org">three.js</a>, one of the most popular open-source web projects. Some of the reasons behind this include:</p><ul><li>Managing native solutions for iOS and Android platforms would demand extra engineering effort.</li><li>The support and documentation available for three.js surpass others.</li><li>The web view solution delivers native-like performance by using WebGL on both platforms. As a result, we consistently achieve 60/120 FPS across all scenes on both devices.</li></ul><p><strong>The plan was simple. Build simple HTML files that render 3D scenes and load these files locally in mobile apps 🚀</strong></p><h3>But we had a “huge” problem…</h3><p>The 3D models were specifically designed for static illustrations, resulting in a high polygon count that contributes to larger file sizes. For instance, the glTF file for the plane model is approximately <strong>19.3 MB</strong>. In total, the original file size for all the 3D scenes we support was around <strong>324 MB</strong>.</p><p><strong>To make these files suitable for mobile apps, we had to optimise and compress them extensively.</strong></p><h3>Optimisation #1 — Remove duplicate images</h3><p>The first optimisation we have to remove the duplicate textures in glTF files.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MfGhZu-RC7SUmxQdDsW0mw.png" /><figcaption>padlock and globe models use same blue texture</figcaption></figure><p>Both padlock and globe scenes use the common blue texture. If we loaded glTF files directly we would duplicate the texture in mobile app bundle.</p><p>To address this issue, we followed these steps:</p><ol><li>Extract the embedded images from the glTF file.</li><li>Eliminate any duplicate images.</li><li>Recreate each glTF file, ensuring that it references the image files correctly.</li></ol><p><strong>As a result, we successfully reduced the overall asset size from 324 MB to 98.4 MB. While this was a significant improvement, we were confident that we could achieve even better results!</strong></p><h3>Optimisation #2 — Compress textures</h3><p>The original textures extracted from the glTF files were huge. To enhance performance and to reduce the bundle size on mobile apps, we prioritised optimising textures over maintaining high-quality versions. Therefore, we were able to significantly compress the textures.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*32r2UL4dWzmbnNuifCkgsQ.png" /></figure><p><strong>As a result of compressing textures, we managed to further reduce the overall asset size from 98.4 MB to 5.2 MB. While this was impressive progress, we knew we could do even better!</strong></p><h3>Optimisation #3 — Compress geometry data</h3><p>The last step was to compress geometry data and to reduce the glTF file size. We compressed each model using an open source library <a href="https://google.github.io/draco/">Draco</a> from Google.</p><p><strong>Now with this final </strong>optimisation<strong>, we successfully reduced the overall asset size from 5.2 MB to 3.7 MB.</strong></p><h3>324 MB &gt; 3.7 MB</h3><p>After implementing all optimisations, the final mobile bundle size was reduced from <strong>324 MB</strong> to <strong>3.7 MB</strong>!</p><p>This new size was a significant achievement, given the mobile bundle consists of glTF model files, textures, JS libraries (three.js and draco.js) and HTML files.</p><p>All of these optimisations are fully automated through a dedicated internal repository that houses our entire optimisation pipeline. This setup enables us to add new 3D scenes more efficiently and quickly.</p><h3>Displaying 3D scenes on mobile apps</h3><p>Each 3D scene is contained within a standalone HTML file, which loads JavaScript libraries and assets locally. These scenes are then displayed in mobile apps through web views.</p><p>By rendering scenes on WebGL using three.js, we achieve a smooth 60 to 120 FPS, ensuring excellent performance on modern mobile devices without any issues.</p><p>One of the biggest challenges on both platforms was the delay in loading scenes. Since we heavily compress models using the Draco library, decompression takes time. To address this, on both platforms, we pre-load the scenes, store them in memory while rendering is off. This approach ensures instant loading times for a seamless user experience.</p><p>Both iOS and Android platforms have abstracted the integration of these 3D scenes. When a mobile engineer wants to use a 3D scene, they simply specify the desired 3D scene, and our UI components handle the rest automatically.</p><h3>Interactivity on mobile apps</h3><p>HTML files featuring 3D scenes also support interactivity, offering APIs to control various aspects such as starting or stopping rendering, managing animations, and adjusting rotation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/480/1*9_yo-_D5ece8tWpZHURYsw.gif" /><figcaption>This is the screen recording captured while I was physically rotating the device.</figcaption></figure><p>Mobile apps gather rotation data using their internal gyroscope natively and send this information to the HTML files, enabling the animation of rotation in the 3D scene.</p><p>And that’s it! This is how we turned a simple idea into an actual feature and made our illustrations 3D interactive!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/1*7kn5V7ilcWJY1LALTfV_dw.gif" /></figure><p>A big thank you to <a href="https://medium.com/u/37f2b52898c0">Chao Quan Choo</a> and <a href="https://medium.com/u/1bac926babfc">Ness Grixti</a> for their hard work and dedication in pushing this project forward and making it happen!</p><p>A big shout out to the folks at <a href="https://raggededge.com">Ragged Edge</a> for their amazing work in designing the illustrations!</p><p>Onwards 🚀</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=515c3de87b74" width="1" height="1" alt=""><hr><p><a href="https://medium.com/wise-engineering/how-did-we-build-3d-animations-in-mobile-apps-515c3de87b74">How did we build 3D animations in mobile apps?</a> was originally published in <a href="https://medium.com/wise-engineering">Wise Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Grafana Mimir Compaction: From Bottleneck to Savings]]></title>
            <link>https://medium.com/wise-engineering/grafana-mimir-compaction-from-bottleneck-to-savings-b26c6b0125a6?source=rss----f2565bbe9c46---4</link>
            <guid isPermaLink="false">https://medium.com/p/b26c6b0125a6</guid>
            <category><![CDATA[observability]]></category>
            <category><![CDATA[metrics]]></category>
            <category><![CDATA[grafana-mimir]]></category>
            <category><![CDATA[platform-engineering]]></category>
            <category><![CDATA[platform]]></category>
            <dc:creator><![CDATA[Milon James]]></dc:creator>
            <pubDate>Wed, 15 Jan 2025 11:55:57 GMT</pubDate>
            <atom:updated>2025-01-15T11:55:57.634Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SzCCT4gRbJhfRUtQhQUEIQ.jpeg" /></figure><h4><strong>Intro</strong></h4><p>This technical blog article dives deep into a compaction bottleneck that was silently impacting our metrics infrastructure after we migrated from Thanos to Grafana Mimir. Follow along as we detail the investigation, solution, and the remarkable performance and cost improvements that transformed our metrics pipeline.</p><p>To give readers some context on the scale of our operations at Wise, our production Mimir stack is currently ingesting approximately a total of <strong>6 million metric samples per second</strong> across different tenants at the time of writing — a testament to the critical role it plays in our observability infrastructure.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2Wbjj9QMdKkbmJgAeye2xA.png" /><figcaption>Write throughput and latency</figcaption></figure><p>In the world of observability, managing large-scale metric storage systems can be a challenging endeavour. Recently, Wise made a significant shift from <strong>Thanos</strong> to <strong>Grafana</strong> <strong>Mimir</strong>, a decision driven by our growing need for better scalability, feature richness, and operational simplicity. The switch has already yielded several benefits, but a critical issue with <strong>Mimir compaction</strong> brought new challenges to light. Once resolved, this fix not only improved system reliability but also delivered substantial cost benefits in terms of <strong>S3 storage savings</strong>, <strong>cross-AZ network transfer reductions</strong>, and <strong>query latency improvements</strong>.</p><p>Here’s an overview of our journey — from the switch to Mimir to resolving compaction issues and reaping the rewards.</p><h4><strong>Why We Switched from Thanos to Mimir</strong></h4><p>Thanos had served us well as a metrics storage and query system, but as our use case evolved, we began to encounter limitations:</p><ol><li><strong>Scaling Challenges</strong>: Managing Thanos components at scale — especially with the growth in the volume of metrics data — became operationally complex.</li><li><strong>Retention and Compaction Flexibility</strong>: Thanos’s approach to compaction and retention felt rigid for our growing requirements.</li><li><strong>Feature Set</strong>: Mimir provided robust multi-tenancy capabilities, advanced query performance optimisations, and seamless integration with our existing workflows.</li></ol><p><strong>Benefits of Switching to Mimir</strong></p><p>Since adopting Mimir, we’ve realized several advantages:</p><ul><li><strong>Operational Simplicity</strong>: Mimir’s architecture reduced the number of different components we had to manage, making operations more straightforward.</li><li><strong>Improved Query Performance</strong>: Features like sharding and efficient query path optimizations resulted in faster query response times — <strong>75% reduction in average query latency during peak usage</strong>.</li><li><strong>Seamless Multi-Tenancy</strong>: Native support for multiple tenants aligned perfectly with our requirements for isolating workloads across teams.</li><li><strong>Advanced Retention Policies</strong>: Mimir’s retention mechanisms allowed us to fine-tune data retention based on tenant-specific needs, reducing unnecessary storage.</li></ul><p>Despite these benefits, the shift to Mimir wasn’t without its challenges. One major issue arose around <strong>compaction</strong>, which became a priority to address.</p><h4><strong>Problem &amp; Symptoms</strong></h4><p>After transitioning to Mimir, we noticed the following critical operational inefficiencies that needed attention:</p><ol><li><strong>Query Performance</strong>: Increased error rate(5XXs) and latencies for long time-range queries, even though we have <strong>extensive caching</strong> in place.</li><li><strong>Resource Usage</strong>: Mimir read path components (store-gateway, querier etc) demanding more and <strong>more compute</strong> resources.</li><li><strong>Cross-AZ Cloud Network Costs</strong>: Elevated data transfer between availability zones hinted at inefficiencies in data processing.</li><li><strong>S3 Storage Costs</strong>: Storage utilisation was increasing disproportionately, with redundant data lingering in object storage.</li></ol><p>These symptoms did not initially suggest that Mimir compaction was the bottleneck, nor did it cross our minds as the likely cause. However, during troubleshooting, we came across a comment in one of the GitHub issues¹ flagged for Mimir. This comment provided a key insight into what the potential issue could be and became a turning point in identifying the root cause.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fxVV5sveIhDVb6jWsPX1bw.png" /><figcaption><a href="https://github.com/grafana/mimir/discussions/4696#discussioncomment-8647567">https://github.com/grafana/mimir/discussions/4696#discussioncomment-8647567</a></figcaption></figure><h4><strong>Detection</strong></h4><p>The first signs of the problem emerged through <strong>Mimir monitoring metrics</strong>, prompting us to dig deeper. We visualised the volume of non-compacted data being queried via the <strong>store gateways</strong>, and the numbers were staggering — our stack was handling around <strong>250k blocks</strong> during the period of the issue.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nu6_aAJhs-kS67TpFxvBCA.png" /><figcaption>Non-compacted blocks fetched while querying</figcaption></figure><p>Additionally, we noticed a clear trend: the number of blocks grew proportionally with the increasing volume of metrics data and the scaling efforts we made to improve the throughput of the <strong>ingester component</strong> on the write path.</p><p>For context, it is the <strong>compactor’s responsibility to merge and de-duplicate</strong> <strong>the blocks</strong> shipped by the ingester replicas. This insight pointed directly to compactor performance issues as the root cause of the growing inefficiencies.</p><h4><strong>Root Cause</strong></h4><p>We rely heavily on the <strong>mixin dashboards, alerts</strong> <strong>and runbooks³</strong> provided by Grafana for monitoring our Grafana components. These tools have consistently served us well, offering robust insights and timely alerts. However, in this specific case, we did not receive any alerts indicating that the <strong>compactor was underperforming</strong>, nor did the dashboards provide clear clues about the situation.</p><p>This lack of direct signals meant we had to dig deeper to understand why the compactor was not performing as expected.</p><p>The <strong>Grafana Mimir official docs and runbooks²</strong> proved invaluable during this process. It provided detailed explanations of how <strong>Mimir compaction</strong> <strong>works³</strong> and offered guidance on how to <strong>tune the compactor³</strong> for better throughput and performance. This deeper understanding was crucial in helping us identify the root cause and implement the necessary fixes.</p><blockquote><em>“</em>A tenant should not have a steadily increasing number of blocks. A pattern of growth followed by compaction is normal. Total block counts can also be examined but these depend on the age of the tenants in the cluster and sharding settings. Values from &lt;1200 blocks upward could be normal. 50K blocks would generally not be normal.<em>”²</em></blockquote><p>Our situation mirrored the challenges described above. In our case, <strong>compaction was functioning well</strong> for all the smaller tenants, but it struggled significantly with our largest tenant. Over time, this led to a <strong>backlog of un-compacted blocks spanning over two months</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3qW7sTvLDczzdA0osYAjpg.png" /><figcaption>Avg blocks/tenant</figcaption></figure><p>As a result, the number of<strong> blocks in object storage</strong> kept growing steadily, exacerbating the performance and cost issues we were facing. This imbalance between smaller tenants and the large tenant made it evident that the compactor’s performance was not scaling adequately with the increased workload.</p><h4><strong>Underperforming — why ?</strong></h4><p>As we were still learning the intricacies of the new tool, the <strong>compactor component</strong> was somewhat overlooked during our efforts to stabilise the stack for <strong>writes and reads</strong>. Our focus had been on ensuring reliable ingestion and query performance, inadvertently leaving the compactor’s optimisation lower on the priority list.</p><p>This oversight became evident as the system scaled and the compactor struggled to keep up with the demands of our largest tenant, highlighting the importance of a holistic approach when adopting new tools.</p><p>The <strong>late detection</strong> of the issue was largely due to a <strong>lack of relevant alerts</strong>. While our monitoring stack provided visibility into many aspects of Mimir’s performance, there were no specific alerts configured to signal <strong>compactor underperformance</strong> or a growing backlog of un-compacted blocks.</p><p>This gap in alerting delayed our ability to identify and address the problem early, emphasising the need to refine monitoring and alerting configurations to cover all critical components comprehensively.</p><p>The <strong>compactor metrics and logs</strong> provided a clear indication of the issue: the <strong>synchronisation of block metadata</strong> was taking significantly longer to complete for the large tenant. This delay was a direct result of the accumulation of an excessive number of blocks in the object storage.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ByMISxaKkAh-NLoeJpb46g.png" /><figcaption>Compactor Logs</figcaption></figure><p>Whenever a compactor instance starts, it performs a <strong>metadata synchronisation</strong> by scanning all blocks in the object storage. Based on the synchronised metadata, it determines the compaction jobs to execute and then proceeds to run them.</p><p>In our case, the <strong>huge backlog of blocks</strong> for the large tenant caused the metadata synchronisation operation to take approximately <strong>9 hours</strong> to complete. This prolonged synchronisation process <strong>significantly delayed compaction tasks</strong> for the tenant, compounding the issue over time and further increasing the backlog.</p><p>We also realised that the <strong>default settings for the compactor</strong> were not sufficient for our use case, particularly for the large tenant. The configuration was not tuned to handle the high volume of blocks efficiently, and as a result, <strong>some compaction jobs were being skipped</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xDW3mYL_8AptyCTOztof6Q.png" /><figcaption>Compactor Logs</figcaption></figure><h4><strong>The Fix</strong></h4><p>Once we detected the root cause the solution to the problem was more or less straight forward. To resolve these issues, we implemented several targeted solutions:</p><ol><li><strong>Resource Optimisation</strong>: Allocated additional CPU and memory to the compactor, ensuring it could process workloads efficiently.</li><li><strong>Configuration Adjustments</strong>: Next step was to tune the compaction config parameters³ to help clear the backlog and optimise for our setup⁴.</li><li><strong>Trial and Error</strong>: Experimentations in staging (where we had the same issue) &amp; promote whatever worked to production environment.</li><li><strong>Monitor &amp; Repeat</strong>: Test, monitor, get feedback and repeat.</li></ol><p>After several days of careful adjustments and patient monitoring, we finally began to see some positive results. The most notable sign of improvement was the <strong>significant decrease in the total number of blocks</strong> stored in the object storage.</p><p>This reduction indicated that the <strong>compaction process</strong> was finally catching up, processing and merging the accumulated blocks effectively. The backlog was starting to clear, and the system’s performance and storage utilisation began to stabilise, validating the changes we had made.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*l9E_blQR136l2BO0GwU1HA.png" /><figcaption>Blocks count in S3 going down</figcaption></figure><p>A few days and <strong>a</strong> <strong>hundreds of compactions later</strong>, we began to see a clear improvement. The overall <strong>block count</strong> in the object storage started to plateau at a <strong>stable number</strong>, indicating that the backlog was finally under control and the compaction process was functioning as expected 😌.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jIcR1pIe3GbItYKdnktbHQ.png" /><figcaption>Blocks count in S3 plateauing after the fix</figcaption></figure><p>This stability was a major milestone, confirming that the adjustments made to the compactor settings and the system’s configuration had successfully addressed the underlying issues. The system was now in a steady state, and the efficiency gains were becoming evident.</p><h4><strong>Impact &amp; Gains</strong></h4><p>A summary of the impact that was observed and the key gains are summarised below.</p><p><strong>Cost Savings</strong></p><ul><li><strong>S3 Storage</strong>: Efficient compaction and retention reduced storage usage by <strong>70%</strong>, directly lowering costs.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0ffDcWwTEFBmis2T5AJNuw.png" /><figcaption>S3 storage utilisation</figcaption></figure><ul><li><strong>Cross-AZ Transfers</strong>: Optimisations led to a <strong>40% reduction</strong> in network traffic between availability zones, significantly cutting cloud networking expenses.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Een_th1xdnXGW2REUiYj7Q.png" /><figcaption>xAZ transfer costs</figcaption></figure><ul><li><strong>Compute Efficiency</strong>: More <strong>efficiency</strong> in metrics stack, by enabling us to significantly scale down the memory requirements for some of the Mimir read path components especially store-gateway and queriers</li></ul><p><strong>Performance Improvements</strong></p><ul><li><strong>Query Latency</strong>: Query response times improved by an average of <strong>90%</strong>, providing a smoother experience for end-users.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1018/1*tiHmbwFLuLsqEFQT5Us3gw.png" /><figcaption>Query-Frontend 99p Latency</figcaption></figure><ul><li><strong>System Reliability</strong>: With compaction running smoothly and consistently, unexpected failures and latency spikes were eliminated.</li></ul><p><strong>Operational Simplicity</strong></p><ul><li>Fewer anomalies and clearer metrics made it easier to monitor, maintain, and scale the system. This experience also brought valuable <strong>learnings</strong> and a deeper <strong>confidence</strong> in operating Mimir at scale. We gained a better understanding of how to fine-tune the compactor and other components to handle large datasets more effectively.</li></ul><h4><strong>Key Takeaways</strong></h4><p>Before ending this blog post we would like to highlight some of the key learnings that we had from this exercise and also the actions that we are taking to make our metrics stack more robust, resilient and reliable.</p><ul><li><strong>Learnings</strong>:<br>- The importance of monitoring block growth and compaction behaviour.<br>- We should do a periodic review of optimised resource allocation for all the components.</li><li><strong>Ongoing efforts</strong>:<br>- Prevent recurrence of such issues by implementing relevant alerts for early detection and proactive monitoring.<br>- Extend Grafana Mimir runbooks for our setup.</li><li><strong>Future Actions</strong>:<br>- Check-box exercise against Grafana Mimir production⁵ tips recommendations.<br>- Apply the key learnings from this experience to other Grafana products with similar architectures that we maintain, such as Loki, Tempo, and others.</li></ul><h4><strong>Conclusion</strong></h4><p>Our transition from Thanos to Grafana Mimir was motivated by a need for better scalability, flexibility, and operational simplicity. While the journey wasn’t without challenges, addressing the compaction inefficiencies reaffirmed the value of the switch. The resolution brought measurable cost savings, improved reliability, and enhanced performance, reinforcing Mimir as the right choice for our growing observability needs.</p><p>If your organisation is considering a similar migration or facing similar engineering challenges with metric storage systems, feel free to share your experiences or reach out for a discussion.</p><p>If you are interested in learning more what we do, check our tech blog on <a href="https://medium.com/wise-engineering">Medium</a>, and <a href="https://www.wise.jobs/engineering?utm_source=engineeringblog&amp;utm_medium=blog&amp;utm_campaign=careers_site">check out our open Engineering roles here</a> if you wish to become a part of our team!</p><p>[1] <a href="https://github.com/grafana/mimir/discussions/4696#discussioncomment-8647567">Gaps when displaying metrics in Grafana · grafana mimir · Discussion #4696</a></p><p>[2] Grafana Mimir runbooks &amp; mixins<br><a href="https://grafana.com/docs/mimir/latest/manage/mimir-runbooks">Grafana Mimir runbooks | Grafana Mimir documentation</a><br><a href="https://github.com/grafana/mimir/tree/main/operations/mimir-mixin">https://github.com/grafana/mimir/tree/main/operations/mimir-mixin</a></p><p>[3] How Grafana Mimir compaction works.<br><a href="https://grafana.com/docs/mimir/latest/references/architecture/components/compactor/">Grafana Mimir compactor | Grafana Mimir documentation</a><br><a href="https://grafana.com/blog/2022/04/19/how-grafana-mimirs-split-and-merge-compactor-enables-scaling-metrics-to-1-billion-active-series/">How Grafana Mimir’s split-and-merge compactor enables scaling metrics to 1 billion active series | Grafana Labs</a></p><p>[4] <a href="https://github.com/grafana/mimir/discussions/3369#discussioncomment-11303497">Scaling / Configuration recommendations for compactor · grafana mimir · Discussion #3369</a></p><p>[5] <a href="https://grafana.com/docs/mimir/latest/manage/run-production-environment/production-tips">Grafana Mimir production tips | Grafana Mimir documentation</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b26c6b0125a6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/wise-engineering/grafana-mimir-compaction-from-bottleneck-to-savings-b26c6b0125a6">Grafana Mimir Compaction: From Bottleneck to Savings</a> was originally published in <a href="https://medium.com/wise-engineering">Wise Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Measuring meaningful availability / uptime of Wise]]></title>
            <link>https://medium.com/wise-engineering/meaningful-availability-uptime-of-wise-b1439ddc6270?source=rss----f2565bbe9c46---4</link>
            <guid isPermaLink="false">https://medium.com/p/b1439ddc6270</guid>
            <category><![CDATA[monitoring]]></category>
            <category><![CDATA[availability]]></category>
            <category><![CDATA[platform]]></category>
            <category><![CDATA[uptime]]></category>
            <dc:creator><![CDATA[Dániel Élő]]></dc:creator>
            <pubDate>Fri, 30 Aug 2024 08:24:13 GMT</pubDate>
            <atom:updated>2024-08-30T08:29:16.899Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*hkJ2M3rqODk69whE" /></figure><p>At Wise we’re building the best way to move and manage the world’s money — making it faster, cheaper, convenient and transparent. Product uptime and availability is contributing to three parts of our mission:</p><ul><li>An honest and meaningful uptime report is key to drive <strong>transparency</strong> of our systems.</li><li>High availability is one of the greatest factors in making our product <strong>convenient</strong>. It is very hard to be convenient if the product is not working as designed.</li><li>High availability means less retries and less waiting, driving us closer to <strong>instant</strong> too.</li></ul><p>In this post we will talk about the reporting aspect of uptime and availability, focusing on transparency. We would like to share our journey and various technical considerations towards measuring meaningful availability/uptime for Wise.</p><p>Wise has a wide range of product offerings, condensing the availability or uptime of our business flows is not trivial. These flows consist of multiple business operations with various branches depending on user intent or group.</p><p>Providing a single number as business flow availability or uptime is always going to be an approximation, but we can and should be transparent about how we aggregate and what that represents.</p><p>Our goal is to have a meaningful measurement we can get behind as representative of user-experienced uptime/availability.</p><h3>Definitions</h3><p>First things first, let’s define some terms we’ll use:</p><ul><li><strong>Business flow:</strong> A high level, complex use-case of the Wise product, e.g. “send money” (we aim to measure the availability of this, as this is the most meaningful for the end-users)</li><li><strong>Business operation</strong>: an individual operation a user can perform as part of one use case or business flow. A high level piece of functionality e.g. get a quote, set up a transfer, pay in for the transfer</li><li><strong>API endpoint:</strong> a technical term. Which HTTP(S) API to invoke in order to perform a Business operation. Not a <em>service</em> or a series of calls, a single HTTP(S) endpoint to perform a (part or whole) of Business operation.</li><li><strong>Availability</strong>: A ratio of successful operations (i.e not server errors) to total occurrences for a given time-frame. E.g. 9943/10000 = 0.9943</li><li><strong>Downtime</strong>: Number of seconds the application produces errors, behaves erroneously, or is not reachable.</li><li><strong>Uptime</strong>: Ratio of number of seconds the application did not produce errors, or behaved erroneously to the total number of seconds of the observed period.</li></ul><h3>Uptime vs. Availability</h3><p>People often use these two terms interchangeably. However, they mean different things. Let’s build from the definition:</p><p>Availability and uptime are both a ratio (%) of “good to total”.</p><p>However it is very important to recognize that their viewpoint is totally different:</p><p><strong>Availability is representative of the share of successful operations</strong> (regardless of their distribution in time).</p><p><strong>Uptime is representative of the share of time where we could successfully complete operations </strong>(regardless of how many operations were actually performed).</p><p>Naturally if we have a perfectly even traffic pattern (e.g. exactly one operation attempted every minute all the time forever) uptime and availability will be the same nominal number.</p><p>In real-life application traffic is almost never perfectly even. There are daily/weekly/monthly (and so on) seasonalities, and spikes and valleys in traffic. If we measure the real traffic of a real application, its uptime and availability will never be the same nominal number! They will represent the same reality, but from different perspectives.</p><p>In general, if outages happen during high-traffic times then uptime will be a higher number, whereas if they happen during low-traffic times availability will be higher (for the same outage).</p><p>For example given the following operations/errors distribution:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*7bBXTGY2RLyJ5oxc" /></figure><ul><li>Downtime: 100 seconds</li><li>Total time: 420 seconds</li><li>Successful operations: 30</li><li>Total operations: 44</li></ul><p>Gives <strong>availability</strong> of 30/44 = <strong>68.2%</strong> and <strong>uptime</strong> of (420–100)/420 = <strong>76.2%</strong></p><p>But if the same downtime happened over a different traffic distribution:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*LHazx82HzzU2AZs4" /></figure><ul><li>Downtime: 100 seconds</li><li>Total time: 420 seconds</li><li>Successful operations: 33</li><li>Total operations: 40</li></ul><p>Gives <strong>availability</strong> of 33/40 = <strong>82.5% </strong>and <strong>uptime</strong> of (420–100)/420 = <strong>76.2%</strong></p><p>There is no right or wrong choice, but given that people often turn ratios into “downtime seconds” in their heads, we believe it is better to report uptime to support this subconscious translation.</p><h3>Simplifications</h3><p>Rigorously pairing all operations to users performing them, ordering them and painting the various user journeys (which are different for each user), and deciding success or failure of these journeys is a complex and resource intensive endeavour — especially for a large user base, and a large set of product offerings.</p><p>Instead, approximating this value based on general user behaviour is much more feasible and can still produce meaningful results. In general, this is what the industry does. Not everyone is going for this much detail, but we believe in transparency and honesty even if it means we report “less nines”. We measure key business operations performed by our full user-base and use that information as a proxy to our overall business flow availability.</p><h3>Ways to measure business flow uptime</h3><h4>Synthetic tests</h4><p>A common practice to measure a flow is to set up an automation that behaves as a real production user, and performs a series of predefined set of business operations (the user journey) and log the outcome of the journey as a whole.</p><p>This approach is very easy to set up, and the frequency of these test runs are completely in our control. We can set up a perfectly even test schedule (e.g. once a minute) so it can report both uptime and availability (as with the perfectly even traffic they are the same nominal number). All in all easy to set up and report. Wise has been using such black-box approach for years.</p><p>The black box test served us quite well when we had a simpler product offering (one basic journey of sending money).</p><p>However convenient it was, the black box testing approach has shortcomings:</p><ol><li><strong>Does not scale well for multiple business flows (or multiple branches of a flow), as if we ramp up variations (and leave frequency intact)</strong> we’d generate so much load on our system for just the “test” that our actual users become negligible load. This is one of the reasons why some other companies (e.g. Google) are also moving away from synthetic monitoring and gravitate towards real user monitoring.</li><li>When the product improves, APIs change. So the test needs immediate upgrade. The test can also break when APIs are retired or changed around. This can be manageable so long as there are only a few of these blackbox tests (ties back to scaling issues).</li><li>When the tester tool is down we cannot calculate or backfill data for the lost time period (that we need for accurate reporting).</li></ol><p>This has happened a few times with Wise. Some internal rules of some internal API had changed, and suddenly we needed an extra step in the user journey to fulfil strong customer authentication. The test itself was down. Real users did not experience downtime, but our tooling suggested so. In these cases we had to choose from the following options:</p><ul><li>Assume that a few minutes (or hours) never happened as we have no reliable data</li><li>Assume we were “up” based on user contacts and logs of real user interactions</li><li>Report outrageously low numbers even though the system was up, because the measurement tool was down.</li></ul><p>Usually we opted for the first option and had a small asterisk in the report warning about resolution issues. Then we would run our incident process to make sure we catch these earlier next time without user impact. Then, next time, something completely different broke the test without user impact. As we got more and more processes around our tests, these got less and less frequent but still happened. New errors always find a way.</p><p>Test users are often handled differently than real users by backend applications.</p><p>In our example we had a test user (in the live production database) that was creating 1000+ transfers — every single day. Some of our KYC (Know Your Customer) and AML (anti-money laundering) systems regularly look for suspicious transactions and behavioural patterns to help prevent malicious actors misusing or taking advantage of our systems. The test user had to be exempted, as it would’ve tripped all the alarms. So in this sense our measurement (in order to keep the company running) was no longer doing the exact same thing users do… It did most of it, but had to pass fewer checks. What if those checks we skipped had bugs? We’d miss reporting their downtime. So for these reasons we turned away from synthetic availability/uptime measurements.</p><h4>White Box testing: track business operations of real users</h4><p>These shortcomings above made us consider turning away from synthetic load and blackbox testing to measure organic load of real users. This approach is more representative of user-experienced availability and uptime, and scales much better for our vast product offering. However it comes with its own trade-offs.</p><p>Traffic is no longer perfectly even, as people tend to move money in their business hours, so we have quite the seasonality depending on which part of the world is in office hours at any given time. So uptime and availability is no longer the same nominal number.</p><p>Measuring uptime with real world traffic is not as straightforward as availability.</p><p>As discussed above, given that people often turn ratios into “downtime seconds”, we believe it is better to report uptime to support this subconscious translation. In the next sections we will focus on getting uptime/downtime from real user actions.</p><p>Most real users interact with our API through our clients, which have aggressive retries and fallbacks to make their experience smooth even if the backend is having issues. Transient API errors are not reflected in our website/mobile app experience. However this is very hard to factor in. From our backend’s perspective, retries are independent requests; one fails, the other succeeds. We also have a large customer base who are directly consuming our production API (Wise Platform is used by many partners across the globe), so we cannot rely on their retries.</p><p>All in all, counting even the transient errors is accurate for direct API customers, and even though it is a less nominal uptime than the ones our web and mobile users actually experience it is a sufficient “at least this good, probably better” number</p><h3>Downtime vs. approximate downtime of operations</h3><p>Calculating real downtime from real (non-generated) traffic needs us to enumerate all requests and count the times elapsed between first failed and the next successful request. This is only easy if you have a single queue of requests. Distributed systems designed to be redundant and reliable are just not that simple — they are never “completely down”: some parts might work while others don’t at the same time.</p><p>A simple way of approximating downtime is to aggregate away the traffic-pattern of availability. This will not produce the exact same result, but is much easier to calculate and reasonably similar.</p><p>Given a short time window (in Wise, 1 minute) we can consider the traffic “constant”. With that assumption we can represent the uptime of an API endpoint for a short period by the availability of it. To get its downtime, we simply subtract the uptime from the window length itself.</p><p>The smaller this time window is the better the approximation. 5 minute window is commonly used. Wise used 2 minutes for a few months, and then we have improved to use 1 minute windows.</p><p>From there, to get the downtime of a meaningful period we simply sum up the downtime of the small windows the period consists of.</p><h3>Aggregate downtime of operations to flows</h3><p>As described above we have a good way of approximating the downtime of each and all important business operations (that each have a single API endpoint we can measure the availability of). The general public (customers, auditors, stakeholders) want a “Single number” representing the full business flow uptime — not 8–10 uptimes for the operations it consists of. To get that reportable number we need to make a trade-off:</p><p>First of all let’s declare what do we mean by “down” for a business flow: The business flow is down if at least one of the operations it consists of is down.</p><p><strong>Option one: “the sum of downtimes of all operations in a business flow is the overall downtime of the flow”.</strong></p><p>This assumes that each operation was down at strictly separate times. I.e. at all times there were exactly 1 operation down.</p><p>This is a good “worst case” scenario. It is the equivalent of “<em>at least this good, but probably better</em>”.</p><p><strong>Option two: “the highest of downtimes of all operations in a business flow is the overall downtime (i.e. minimum uptime represents all)”.</strong></p><p>This assumes that all operations were down at exactly the same time. No exceptions. Either everything worked or nothing.</p><p>This is a kind of “best case” scenario, and for simpler business flows it might be the closest to the truth. Engineers don’t really like it as it is very rare that everything is down. Mostly this is too kind to the business. It is the equivalent of “<em>we were down for at least this amount of time, and maybe some more, but this is the biggest contributor</em>”</p><p><strong>Option three: “the average of downtimes of all operations in a business flow is the overall uptime”.</strong></p><p>This aggregation tries to make up outliers by averaging them out. It is used by some key players, e.g. <a href="https://developer.revolut.com/docs/guides/build-banking-apps/api-metrics">Revolut</a> does <em>“Daily uptime is defined as the percentage of minutes in the day where the</em><strong><em> average technical error rate</em></strong><em> was below 5%.”</em></p><p>They do an extremely detailed breakdown though and they are transparent about this averaging.</p><p>At Wise we believe it is always too kind to the business to do averages of business operation uptime/availability for operations that are part of the same business flow, as it will always be more “up” than option one, which we know is a good minimum.</p><p><strong>Option four: “a middle point of the known good minimum and known good maximum”</strong></p><p>I.e. We say we are sure the downtime of a business flow is always between the sum of its operations (option 1) and max of them (option 2). So we get the middle point (average) of this number, and highlight our confidence interval.</p><p>Example:</p><p>Uptime of business operations for 2024 Q2:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/628/1*QUhAtebbTpMcHknHBYlUxw.png" /></figure><ul><li>Minimum uptime of any single operation: 99.958%</li><li>Sum of all downtimes: 7514.5 seconds (There were 7862399 seconds in 2024/Q2) making uptime 99.904%</li></ul><p>So we know that our API customers were able to complete the full flow between 99.904% and 99.958% in 2024/Q2.</p><p>The middle point of it is <strong>99.931% (with +- 0.027% confidence band)</strong></p><p>How Wise will report business flow uptime from now on:</p><ul><li>We will shift where we can to report uptime, not availability</li><li>We will report our API uptime regardless of our client-side web/mobile retries as we have a large direct API customer base and deciding a threshold of transient issues would just cause more confusion for a slightly greater nominal uptime value that only applies to some of our customers.</li><li>We track our uptime per business operation</li><li>We aggregate that number for business flows:</li><li>We add all downtimes of all operations of a flow to get the worst possible value (assuming exactly one of them were down the same time)</li><li>We choose the maximum downtime of a single operation (assuming all of them were down the same time)</li><li>Then since neither exactly 1, nor all of our operations are down at the same time, we average these numbers to get a middle point and report the difference between min/max and average for transparency.</li></ul><p>Onwards.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b1439ddc6270" width="1" height="1" alt=""><hr><p><a href="https://medium.com/wise-engineering/meaningful-availability-uptime-of-wise-b1439ddc6270">Measuring meaningful availability / uptime of Wise</a> was originally published in <a href="https://medium.com/wise-engineering">Wise Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Avoiding Kubernetes Pod Topology Spread Constraint Pitfalls]]></title>
            <link>https://medium.com/wise-engineering/avoiding-kubernetes-pod-topology-spread-constraint-pitfalls-d369bb04689e?source=rss----f2565bbe9c46---4</link>
            <guid isPermaLink="false">https://medium.com/p/d369bb04689e</guid>
            <category><![CDATA[platform-engineering]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[cloud]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Matthew Kennedy]]></dc:creator>
            <pubDate>Tue, 28 Mar 2023 09:12:41 GMT</pubDate>
            <atom:updated>2023-03-28T09:58:31.062Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9P9JJSvv8TgvRdsYFlnSlw.png" /></figure><p>In the Wise Cloud Platform squad we take resiliency, capacity planning and costs seriously. We are always looking for ways to improve how services running on our self-managed Kubernetes clusters in AWS can be made more resistant to system failures without significantly increasing cost.</p><p>One of the mechanisms we use are <a href="https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/">Pod Topology Spread Constraints</a>. This is a built-in Kubernetes feature used to distribute workloads across a topology domain, such as Availability Zones or Nodes.</p><p>In this blog post, we consider Availability Zones to be our topology domain. We discuss how to configure Pod Topology Spread Constraints correctly and how a naive configuration creates unexpected Pod skew.</p><p>If Pod Topology Spread Constraints are misconfigured and an Availability Zone were to go down, you could lose 2/3rds of your Pods instead of the expected 1/3rd. This will likely negatively impact your customers&#39; ability to use your product.</p><h4>What is wrong with Kubernetes Pod Topology Spread Constraints?</h4><p>Skew is the difference in the number of pods between the most populated and least populated Availability Zone. If we have 3 Availability Zones and 3 Pods, ideally we want a skew of 0, as seen in the diagram below.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*lQFQN0Oa1_O4Uav8" /><figcaption>Diagram showing skew of 0</figcaption></figure><p>If we were to add another pod, it could be placed in any of the Availability Zones and our pods would still be distributed as evenly as possible. Our skew would then be 1.</p><p>If the pods are not evenly distributed, the skew will always be greater than 1. In the case of the diagram below, Availability Zone 1 contains 2 pods and Availability Zone 3 contains zero pods, so the skew is 2.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*O04VTIcW5fDY5mtM" /><figcaption>Diagram showing skew of 2</figcaption></figure><p>You can configure the Pod Topology Spread Constraints through the topologySpreadConstraints section of a Pods’ spec. The Pods to which the configuration is applied is determined through <a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors">LabelSelectors</a> and what you deem an acceptable distribution is determined by the maxSkew field.</p><p>Below is an example of a naive topologySpreadConstraints configuration:</p><pre>apiVersion: v1<br>kind: Pod<br>metadata:<br> labels:<br>   app: example-service<br>spec:<br> topologySpreadConstraints:<br>   - labelSelector:<br>       matchLabels:<br>         app: example-service<br>     maxSkew: 1<br>     topologyKey: topology.kubernetes.io/zone<br>     whenUnsatisfiable: DoNotSchedule</pre><p>With this configuration, if we have at least 3 pods, we expect to have a minimum of 1 Pod running in each Availability Zone and our skew to never exceed 1.</p><p>To understand why we saw a deviance from what was expected, we need to explain how scheduling occurs in Kubernetes. A component called <a href="https://kubernetes.io/docs/reference/command-line-tools-reference/kube-scheduler">kube-scheduler</a> decides which Nodes a Pod will be scheduled on, and a configurable set of rules informs where a pod is allowed to be scheduled. However, the problem is that these rules consider the state of the cluster at <strong>the point in time when scheduling occurs</strong>. Kube-scheduler will not rebalance pods after they are scheduled or consider the rules when removing pods.</p><p>A rolling update of a Deployment in Kubernetes will create a new ReplicaSet version and schedule new Pods and terminate the old Pods. The exact way this rollout happens is configurable, but let us assume we want to schedule 1 Pod at a time. Kube-scheduler will schedule a new Pod and the Deployment Controller waits for this Pod to report itself ready. Once ready, one pod from the old ReplicaSet is terminated and the Deployment Controller requests that another Pod from the new ReplicaSet is scheduled. This process repeats until all Pods are rotated. Note that the termination of Pods is not intelligent in any way; one is chosen at random.</p><p>As LabelSelectors are used to define which pods to apply Pod Topology Spread Constraints to, if you use only the app name or something similar the kube-scheduler will consider Pods from <strong>both old and new ReplicaSets</strong>. This makes it possible for a Pod to be scheduled in a way which violates the given Pod Topology Spread Constraints once a Pod from the old ReplicaSet is terminated.</p><p>Let’s consider an example where we are in the middle of a rollout. A new Pod has just been scheduled and reported ready, and an old Pod terminated. Kube-scheduler is now deciding which Availability Zone it can place the new Pod:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*DSX-uCIKI-MlW92B" /><figcaption>Diagram showing a Pod that is about to be scheduled during a rolling update</figcaption></figure><p>According to kube-scheduler the current skew is 0, and it is allowed to place the Pod in <em>any</em> Availability Zone without violating the Pod Topology Spread Constraints. If it places the Pod in one of the Availability Zones that already contain a new Pod, we will end up in a state that violates the constraints once the remaining old Pod is terminated.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*nCdvNNKaerBoJLFg" /><figcaption>Diagram showing how a Pod can be scheduled in an Availability Zone that causes undesirable skew during a rolling update</figcaption></figure><h4>The Solution</h4><p>To prevent this issue, we need kube-scheduler to distinguish between old ReplicaSet Pods and new ReplicaSet Pods. As Pod Topology Spread Constraints use LabelSelectors we can ensure the old and new Pods have a <strong>unique label</strong> in the LabelSelector of our topologySpreadConstraints configuration.</p><p>To do this, we created a <a href="https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/">mutating admission webhook</a> which adds a label, topology.wise.com/id, to the Pod metadata. The value of this label is a uuid. The mutating admission webhook updates the Pod Topology Spread Constraints LabelSelector accordingly whenever a Deployment object passes through the Kubernetes API.</p><p>If our example configuration were passed through this webhook, the result would be:</p><pre>apiVersion: v1<br>kind: Pod<br>metadata:<br> labels:<br>   app: example-service<br>   topology.wise.com/id: &lt;uuid-value&gt;<br>spec:<br> topologySpreadConstraints:<br>   - labelSelector:<br>       matchLabels:<br>         app: example-service<br>         topology.wise.com/id: &lt;uuid-value&gt;<br>     maxSkew: 1<br>     topologyKey: topology.kubernetes.io/zone<br>     whenUnsatisfiable: DoNotSchedule</pre><p>Now, kube-scheduler will not consider the Pods from the old ReplicaSet when scheduling the new Pods.</p><h4>Alternative Approaches</h4><p>Instead of using a mutating webhook, it would have been possible to update tooling to edit the manifests, adding the unique label and associated LabelSelector, before they reach the Kubernetes API. This could have saved us having to deploy and monitor a new component. But to do this we would have to change how deployment manifests are created in every place they are generated, which is not scalable.</p><p>It is also the case that Kubernetes v1.25 adds <a href="https://github.com/kubernetes/kubernetes/pull/111441">a matchLabelKeys field in alpha to the Pod Topology Spread Constraints</a> that can be enabled using a feature gate. Kube-scheduler will assume the values automatically so you can take advantage of the existing pod-template-hash label to achieve the same behaviour we do with the mutating admission webhook.</p><p>Both solutions described will not protect you against imbalances after scaling down a deployment. If you regularly scale down workloads, you may want to consider using a tool like <a href="https://github.com/kubernetes-sigs/descheduler">descheduler</a>.</p><p>You could also consider rearchitecting the way you deploy workloads, such as doing a deployment per Availability Zone or even running a cluster per Availability Zone.</p><h4>Summary</h4><p>Out of the box, Pod Topology Spread Constraints are not enough to ensure even Pod distribution. LabelSelectors that capture Pods from both old and new ReplicaSet versions will cause your availability expectations to be violated. If using Pod Topology Spread Constraints to distribute Pods through Availability Zones, this could mean that in the event of an Availability Zone failure, 2/3rds of a service’s pods are lost.</p><p>We solved this by implementing a mutating admission webhook to add a new label to Pods controlled by a Deployment. This label has a unique ID as a value per ReplicaSet and is referred to in the LabelSelectors of the Pod Topology Spread Constraints configuration. This allows kube-scheduler to distinguish between old and new Pods, preventing us from ending up in an undesirable state.</p><p><em>If you enjoyed reading this post and like the presented challenges, our Platform Engineering team is hiring! </em><a href="https://www.wise.jobs/engineering?utm_source=engineeringblog&amp;utm_medium=blog&amp;utm_campaign=careers_site"><em>Check out our open Engineering roles here</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d369bb04689e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/wise-engineering/avoiding-kubernetes-pod-topology-spread-constraint-pitfalls-d369bb04689e">Avoiding Kubernetes Pod Topology Spread Constraint Pitfalls</a> was originally published in <a href="https://medium.com/wise-engineering">Wise Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How Wise bootstraps Spinnaker using ArgoCD]]></title>
            <link>https://medium.com/wise-engineering/how-wise-bootstraps-spinnaker-using-argocd-2765e9373b61?source=rss----f2565bbe9c46---4</link>
            <guid isPermaLink="false">https://medium.com/p/2765e9373b61</guid>
            <category><![CDATA[spinnaker]]></category>
            <category><![CDATA[argo-cd]]></category>
            <category><![CDATA[continuous-delivery]]></category>
            <category><![CDATA[platform]]></category>
            <dc:creator><![CDATA[Nick Platt]]></dc:creator>
            <pubDate>Mon, 20 Feb 2023 12:24:46 GMT</pubDate>
            <atom:updated>2023-02-20T12:24:46.549Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uVmtooZRfP5NHTHm9QTmFg.png" /><figcaption>Spinnaker and ArgoCD logos</figcaption></figure><p><strong>Deploying your deployment tool</strong></p><p>In our <a href="https://medium.com/wise-engineering/state-of-our-ci-cd-pipeline-part-1-dd5467803718">2 part series</a>, we discussed the state of our CI/CD systems and wrapped up with our vision for CD at Wise, and why we chose Spinnaker. Here’s how we set up Spinnaker from scratch, overcoming the technical challenges of bootstrapping a CD platform.</p><p>We wanted a hands-off and automated way to bootstrap Spinnaker. When researching across industry peers, one approach was to use a staging instance of Spinnaker to manage your production instance. This method of <a href="https://blog.spinnaker.io/running-spinnaker-in-multiple-regions-at-netflix-7eefad1dde7a">dual Spinnakers</a>, wasn’t viable for Wise, due to security concerns with a staging tool having access to change a production environment. It also meant accepting a potentially manual error prone process when managing your staging setup. Instead, we wanted to achieve a fully automatic installation for <strong>both</strong> of our Spinnaker environments and settled on using ArgoCD.</p><p><strong>Spinnaker &amp; ArgoCD</strong></p><p>Spinnaker consists of around 11 Java microservices, which operates on a centralised deployment model — push based deployments. It utilises <strong><em>pipelines</em></strong> heavily as its deployment strategy, which can be triggered via various input methods e.g. manual, artefact upload etc. Implementing deployments as pipelines allows engineers to describe a release as a set of steps, with powerful concepts such as canary analysis and quality gates built in. This is attractive for Wise, as use of standardised and reusable templates can enforce end consumers to follow a particular <a href="https://www.infoq.com/news/2017/06/paved-paas-netflix/"><strong><em>paved road</em></strong></a> e.g. canary deployments with automatic rollbacks.</p><p>ArgoCD describes itself as “<a href="https://argo-cd.readthedocs.io/en/stable/">declarative GitOps for Kubernetes</a>”. It comprises fewer golang microservices and can be configured to be either centralised or decentralised. By default, ArgoCD differs by not utilising pipeline based deployments, instead operating via a <a href="https://www.redhat.com/en/topics/devops/what-is-gitops">GitOps</a> reconciliation loop as its source of truth. Whatever is defined in Git, ArgoCD will try to reconcile within the cluster. ArgoCD is Kubernetes native and therefore can’t be used for deploying infrastructure to other cloud platforms, e.g. VMs, serverless etc.</p><p><strong>Making Spinnaker installation repeatable, toil free and quick</strong></p><p>Each of our new Kubernetes clusters come with an ArgoCD installation. ArgoCD is relatively standalone, installation is done via Helm charts and can be bootstrapped easily.</p><p>To manage our Spinnaker environment we’ve adopted Argo’s <a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/cluster-bootstrapping/">App of Apps</a> pattern — think of it as one master application that instructs Argo to deploy other child apps.</p><p>The below diagram illustrates our setup.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/719/1*CaRsGg1GHgsxik7oVd80Bg.png" /><figcaption>App of Apps definition in ArgoCD UI</figcaption></figure><p>Contained within our app-of-apps are several other ArgoCD Application definitions — these can either point to plain manifests, Helm charts and Kustomize templates etc. We use Kustomize to apply application/environment specific configuration changes. Here’s an example of our Kustomize definition for Spinnaker.</p><pre>apiVersion: argoproj.io/v1alpha1<br>kind: Application<br>metadata:<br> name: kustomize-spinnaker<br> namespace: argocd<br> finalizers:<br> - resources-finalizer.argocd.argoproj.io<br>spec:<br> destination:<br>   namespace: spinnaker<br>   server: https://kubernetes.default.svc<br> project: default<br> source:<br>   path: manifests/kustomize/spinnaker<br>   repoURL: &lt;repo-url-here&gt;<br>   targetRevision: main<br> syncPolicy:<br>   automated:<br>     prune: true<br>     selfHeal: true<br>   syncOptions:<br>   - CreateNamespace=true</pre><p>ArgoCD’s responsibility ends once the manifest definitions are applied to the cluster. If any deployments fail Argo will continuously try to fix the state of the cluster (you can see in our example we have 2 apps with yellow arrows — this indicates the state of the cluster doesn’t match what Argo expects).</p><p>Our deployment contains both the <a href="https://github.com/armory/spinnaker-operator">Spinnaker Operator</a> and Kustomize Spinnaker manifest, the operator includes a CRD (named SpinnakerService) which translates Spinnaker manifests into valid Spinnaker config &amp; Kubernetes objects. You can read more about the operator on Armory’s website, but effectively it bundles <a href="https://spinnaker.io/docs/reference/halyard/">Halyard</a> into the deployment.</p><p>When changes to the SpinnakerService manifest definition are deployed (by ArgoCD), the Spinnaker Operator picks this up and begins rolling out changes: new replicasets, services and ingress. Below, we’ve included a screenshot of our Operator deployment within Argo (it only shows a subset of objects).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/654/1*KaNDMJdJry64KcF4wdXYvQ.png" /><figcaption>Spinnaker Operator definition presented by the ArgoCD UI</figcaption></figure><p>Now whenever we need to deploy to a fresh cluster, it’s as simple as connecting our ArgoCD instance to our Git repository and hitting deploy.</p><p><strong>Setting up mTLS between Spinnaker microservices</strong></p><p>One challenging part of our Spinnaker configuration to automate consistently was our mTLS setup.</p><p>In the case of mTLS, we were bound by some security requirements:</p><ul><li>We didn’t want engineers to see the contents of either private or public keys.</li><li>We didn’t want engineers to have to interact with the key i.e. create and copy them into the cluster manually.</li><li>We didn’t want to setup and manage an internal CA e.g. HashiCorp vault</li><li>We wanted to still use internal cluster DNS e.g. &lt;service&gt;.&lt;namespace&gt;.svc.cluster saving us having to manage new DNS entries.</li><li>Therefore we would need to use a self signed certificate as we don’t own the domain.</li></ul><p>Solving the certificate provisioning issue is relatively easy as we can simply use cert-manager to create our certificates based on manifest definitions. We then need to get our CA &amp; mTLS certificate into the Spinnaker service pods and import our CA into the trust store. Using <a href="https://cert-manager.io/docs/projects/trust-manager/">trust-manager</a> (another cert manager project) we can define a bundle which takes in source files and distributes them to destinations within the cluster, below is an example definition.</p><pre>apiVersion: trust.cert-manager.io/v1alpha1<br>kind: Bundle<br>metadata:<br> name: spinnaker-cert-trust-bundle<br>spec:<br> sources:<br> - secret:<br>     name: selfsigned-certificate-authority<br>     key: &quot;ca.crt&quot;<br> target:<br>   configMap: # what our destination file will look like<br>     key: mtls-cacerts<br>   namespaceSelector: # copy to namespace if label match is true<br>     matchLabels:<br>       kubernetes.io/metadata.name: spinnaker</pre><p>This takes care of getting the CA certificate file into our Spinnaker namespace and mountable by service pods. To then import it into our trust store we used init containers, pulled in the default Java trust store, our CA and an empty directory. An empty directory is necessary as secrets &amp; configmaps are read only so we need a place to store our altered file. After we’ve imported our CA, the empty directory mount is then consumed by the main service container and accepts our self signed CA — here’s all that wrapped up in a manifest.</p><pre>apiVersion: spinnaker.io/v1alpha2<br>kind: SpinnakerService<br>metadata:<br> name: spinnaker<br>spec:<br> kustomize:<br>   gate: &amp;keystore-init<br>     deployment:<br>       patches:<br>         - |<br>           spec:<br>             template:<br>               spec:<br>                 initContainers:<br>                 - name: keystore-init<br>                   image: java-base-image<br>                   env:<br>                     - name: TRUSTSTORE_PASSWORD<br>                       valueFrom:<br>                         secretKeyRef:<br>                           name: certificate-secrets<br>                           key: TLS_INTERNAL_KEYSTORE_PASSWORD<br>                   volumeMounts:<br>                     - name: java-default-truststore<br>                       mountPath: /mnt/default-truststore<br>                     - name: spinnaker-ca-trust-bundle<br>                       mountPath: /mnt/internal-ca<br>                     - name: empty-dir<br>                       mountPath: /mnt/resulting-truststore<br>                   command: [&#39;/bin/sh&#39;, &#39;-c&#39;]<br>                   args:<br>                     - &gt;-<br>                       keytool -importkeystore -srckeystore /mnt/default-truststore -srcstorepass &quot;$TRUSTSTORE_PASSWORD&quot; -destkeystore /mnt/resulting-truststore -deststorepass &quot;$TRUSTSTORE_PASSWORD&quot; --noprompt -v;<br>                       keytool -import -file /mnt/internal-ca -keystore /mnt/resulting-truststore -storepass &quot;$TRUSTSTORE_PASSWORD&quot; --noprompt -v;<br>   …<br></pre><p>Now everytime a new pod is created, it will mount and configure all the necessary files it needs to communicate over mTLS.</p><p>Here’s what our simplified version of our finished implementation looks like:</p><ol><li>Cert manager issues a self signed Certificate which we use as our authority.</li><li>Trust bundler copies the CA to our required namespaces.</li><li>Our CA issuer generates a certificate which we’ll use for mTLS.</li><li>Spinnaker pods automatically mount both certificates, adding the CA to their trust store and begin communicating over HTTPS.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*34uvjbWPiOwE4CiliMGGTA.jpeg" /><figcaption>Technical diagram for automating TLS</figcaption></figure><p><strong>Our vision for managing Spinnaker</strong></p><p>Our production Spinnaker environment will be the norm for how most deployments are done across Wise. We’ll operate it centrally and manage connections for it to reach out and into our other Kubernetes environments. Our staging instance will be a testing ground for us within the Continuous Delivery team to develop and build features into Spinnaker. So, we’ll avoid rolling out broken artefacts and taking down deployments for our entire engineering team.</p><p>And if you’re wondering why we haven’t considered moving deployments entirely to ArgoCD? Well: 1) the need for non-K8S deployments 2) pipeline + release engineering philosophy 3) powerful integrations require additional CNCF projects e.g. <a href="https://keptn.sh/">keptn</a></p><p><strong>What does our deployment workflow for Spinnaker look like with ArgoCD</strong></p><p>We’ve ultimately forked all the microservices (and the Operator + Halyard) to meet our needs. When we are ready to release new changes, our CI system builds and packages images to a container registry. Previous users of Spinnaker will know that it uses a <a href="https://spinnaker.io/docs/guides/operator/custom-boms/">Bill of Materials</a> to determine what versions of each microservice are bundled to make a Spinnaker platform version. Updating this is currently a manual process, along with generating our CHANGELOG, but once this has been merged to master, our CI process takes over once again and stores it in versioned object storage which Spinnaker can later pull from. So, we can easily rollback to older versions if we need to.</p><p>Rolling out these new versions is as simple as updating the version string for Spinnaker in our repo and merging to main. ArgoCD will then sync the latest commit, notice the change of state and reconcile the difference.</p><p>We’re currently exploring evolving our development workflow to incorporate Ambassador labs <a href="https://www.telepresence.io/">Telepresence</a>, to speed up our feedback loop when developing new features into Spinnaker services. Keep an eye out for our findings later in the year.</p><p><strong>Wrapping up</strong></p><p>Our CD team has been hard at work getting Spinnaker into a place where they are confident in moving over identified early adopters and aim to get our migration underway before Q2 of 2023. Beyond trying to automate away our initial App of Apps setup, any future work that we carry out on Spinnaker will now be shaped by requests from our internal engineers as they begin to onboard and use the tool.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2765e9373b61" width="1" height="1" alt=""><hr><p><a href="https://medium.com/wise-engineering/how-wise-bootstraps-spinnaker-using-argocd-2765e9373b61">How Wise bootstraps Spinnaker using ArgoCD</a> was originally published in <a href="https://medium.com/wise-engineering">Wise Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Improving the Developer Experience — Our Application Security Journey (Part 3)]]></title>
            <link>https://medium.com/wise-engineering/improving-the-developer-experience-our-application-security-journey-part-3-757e0e6d32e4?source=rss----f2565bbe9c46---4</link>
            <guid isPermaLink="false">https://medium.com/p/757e0e6d32e4</guid>
            <category><![CDATA[platform]]></category>
            <category><![CDATA[security]]></category>
            <category><![CDATA[application-security]]></category>
            <dc:creator><![CDATA[Florian Wirtz]]></dc:creator>
            <pubDate>Tue, 17 Jan 2023 11:19:44 GMT</pubDate>
            <atom:updated>2023-01-17T11:19:44.920Z</atom:updated>
            <content:encoded><![CDATA[<h3><strong>Improving the Developer Experience — </strong>Our Application Security Journey (Part 3)</h3><p><em>This is the third in a series of articles on the state of Application Security at Wise, describing our integration of security in the Software Development Lifecycle.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/0*pvmxpKeWgInCyjgD" /><figcaption>Photo by <a href="https://unsplash.com/@possessedphotography">Possessed Photography</a> on <a href="https://unsplash.com/photos/tiNCpHudGrw">Unsplash</a></figcaption></figure><p>In part three of our blog post series we will be focusing on how we can improve the developers’ experience around fixing vulnerabilities. We explore how we identified the main pain points developers were facing, what we did to improve this experience and future improvement ideas that we are planning to work on.</p><h3>Recap: What happened so far?</h3><p>Over the course of the last year we have created our new setup for identifying vulnerabilities at Wise. It’s centred around DefectDojo as our vulnerability management tool and we use scanners, such as <a href="https://aquasecurity.github.io/trivy/">Trivy</a> for Software Composition Analysis, as inputs. You can <a href="https://medium.com/wise-engineering/our-application-security-journey-part-1-fb7d449a7126">learn more about our setup in part one of this blog series</a>.</p><p>Since then we have also created various dashboards to report vulnerabilities to our stakeholders, and also improved our Service-level agreement (SLA) with our developers to resolve new vulnerabilities. To learn more about that, <a href="https://medium.com/wise-engineering/our-application-security-journey-part-2-5b1a3028f260">please read part two of this series</a>.</p><h3>What issues are our developers facing?</h3><p>As a next step, it’s important to check in with our developers to see how they’re interacting with our program. It’s one thing to set up a vulnerability management program, but we also need to make sure that it actually works for our users and that the recommendations provided are actioned in a timely manner.</p><p>While the initial feedback we received from developers was promising, we decided to share a survey with them to collect more actionable feedback. Our survey had a mix of multiple-choice and free-form questions and was mostly focused on how developers are using our tools and what issues they might be facing with them. We also interviewed them for analogous use cases, including what works well and what doesn’t work well in other tools they’re using. For example, we asked:</p><ol><li>When did you last check the status of your team’s vulnerabilities?</li><li>If you already receive alerts, how do you receive the majority of them?</li><li>How often would you want to be notified about vulnerabilities that were found?</li></ol><p>Our survey concluded that there were two main pain points that developers were experiencing: Our dashboards were slow to load, and second, that teams had to be proactive and needed to remember to check the dashboards we provided them with, sometimes clashing with their already packed schedules and deadlines.</p><p>These two issues raised could potentially result in developers getting easily frustrated by our tooling, but also that they might forget or miss vulnerabilities, because they didn’t check a dashboard in time. The developer experience suffered from that, but it also made it harder for teams to stay within the SLAs that we established.</p><h3>What can we do about it?</h3><p>One simple way to tackle both of these issues at the same time is to introduce alerts. It solves both the speed issue, as developers don’t even need to visit the dashboards, unless we tell them there is something worth seeing there, but also the proactivity problem: Instead of developers constantly needing to check whether there are new vulnerabilities the alerts can just tell them.</p><p>More than half of the survey participants requested to get a weekly report of all vulnerabilities that are approaching their SLA due date, so that they would have enough time left to fix them. In general, team’s ways of working differ widely across the company though, <a href="https://www.wise.jobs/2019/12/05/how-we-work-a-practical-guide/">as our teams have a lot of autonomy</a>. To address that, we added a version-controlled configuration file that teams can append a cron expression to in order to receive notifications on their own schedule.</p><pre>[<br> {<br>   &quot;teamName&quot;: &quot;application-security&quot;,<br>   &quot;slackChannel&quot;: &quot;appsec-alerts&quot;,<br>   &quot;cronSchedule&quot;: &quot;0 9 * * MON&quot;<br> },<br> // ...<br>]</pre><blockquote>Code Snippet: Teams define their team name, preferred slack channel as well as a cron syntax schedule to receive alerts. In the example above, the Application Security team would receive their notifications <a href="https://crontab.guru/#0_9_*_*_MON">at 09:00 am on Monday</a>.</blockquote><p>Almost all of our alerting in the company is delivered via Slack, which is why we decided to create a Slack notification as well. Our new bot now sends teams an automatic vulnerability report whenever they need it, which for most teams means once a week. The report shows all vulnerabilities that will breach their SLA in the next two weeks or less. For each vulnerability, the report also shows some metadata, including how many days are remaining to fix it, what the mitigation is and what the CVE of the vulnerability is.</p><figure><img alt="A screenshot of the bot called “Vuln. shouter” sending a notification on Slack. The sent message contains a vulnerability report, providing more information on two different vulnerabilities." src="https://cdn-images-1.medium.com/max/662/1*wFMmroMHW_tRNwBYGqkYAQ.png" /><figcaption><strong>Figure 1: Screenshot of a vulnerability report notification</strong></figcaption></figure><p>Additionally, teams can report vulnerabilities as a false positive, directly from the slack message, by raising a pre-filled Jira ticket with us. One month after releasing this new feature more than half of all teams in Wise had already signed up to receive these notifications which is a great indicator.</p><h3>What’s next?</h3><p>Currently, the bot still has a few limitations that we are working on improving. Since releasing the bot we have also massively improved the loading speed of our vulnerability dashboards. Over the next months we will continue to improve our service to developers based on the feedback we receive.</p><p>We are also really excited about the developer portal that was recently launched in Wise and is <a href="https://medium.com/wise-engineering/wise-tech-stack-2022-edition-a6ac089a382f">discussed in more detail in our recent wise tech stack blog post</a>. One aspect that’s particularly interesting about it is to display vulnerability data in the developer portal, similar to <a href="https://engineering.atspotify.com/2022/11/spotifys-vulnerability-management-platform/">what Spotify recently described in their post about vulnerability management</a>.</p><p>If you enjoyed reading this post and like the presented challenges, keep an eye out for open Application Security Engineering roles <a href="https://www.wise.jobs/teams/engineering/?utm_source=engineeringblog&amp;utm_medium=blog&amp;utm_campaign=careers_site">here</a>.</p><p><strong><em>Disclaimer</em></strong><em>: Product names mentioned in this article are property of their respective owners. This article is not aimed at publicising or endorsing any of the brands used or mentioned in the article.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=757e0e6d32e4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/wise-engineering/improving-the-developer-experience-our-application-security-journey-part-3-757e0e6d32e4">Improving the Developer Experience — Our Application Security Journey (Part 3)</a> was originally published in <a href="https://medium.com/wise-engineering">Wise Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>