When a “simple” Redis disk resize in Kubernetes turns into a VPN incident: a post-mortem

TL;DR: the short version

On the 2nd of December, 2025 we got an alert that a critical system component (Redis) was running out of disk space. Fixing the capacity issue itself was straightforward. The real trouble started when we tried to “clean things up” afterwards and make the configuration match reality.

Because this component runs in Kubernetes and is set up in a way that doesn’t allow straightforward resource adjustment, we attempted a safe-looking workaround: recreate the controller object without deleting the actual running pods. Kubernetes interpreted that change differently than we expected and restarted the whole Redis cluster. The restart process wasn’t “cautious” enough, it moved too fast and caused the cluster to come back partially degraded. While we were repairing the degraded nodes, a backend service got overwhelmed by reconnect traffic and started failing with out-of-memory errors. After scaling and tuning, the service recovered. Total downtime was ~50 minutes.

What happened

We received an alert that several instances of the Redis cluster were running out of free disk space. But increasing the disk space in our case is associated with certain problems. The thing is, in Kubernetes things such as Redis are usually created via a StatefulSet. It’s specifically designed for workloads that need stable identities and storage.

However, some parts of the StatefulSet are locked, and it can be a problem. StatefulSets define storage via volumeClaimTemplates. Important details:

  • the storage template is effectively locked once created
  • you can’t simply edit it in place to increase disk size

So even if you know exactly what you want, Kubernetes won’t let you change that specific field in the StatefulSet spec.

The immediate fix: increase capacity without restarts

Our storage backend supports online volume expansion. That means we can increase disk capacity underneath a running pod without restarting it.

So we resized the PersistentVolumeClaims (PVCs) directly. This resolved the disk pressure quickly and safely.

But it created a new problem:

  • the declarative configuration (Git + StatefulSet template) was still describing the old disk size
  • the actual PVC/PV state in the cluster now reflected the new disk size

This mismatch is a classic “configuration drift.”

The “cleanup” attempt: make the code match reality

Because the StatefulSet storage template is immutable, the only straightforward way to align the declared state with the real state is to:

  1. Delete the StatefulSet object
  2. Recreate it with the updated storage template

Of course, deleting the StatefulSet normally results in deleting child resources. So we used a Kubernetes trick:

Orphan deletion: “fire the manager, keep the factory running”

We deleted the StatefulSet using orphan / non-cascading deletion. The idea was:

  • Kubernetes forgets the “manager” object…
  • …but the pods (and their disks) stay alive

Then we recreated the StatefulSet so Git and the cluster would be consistent again.

This looked safe — but Kubernetes had a different interpretation.

The unexpected part: Kubernetes restarted the entire Redis cluster

After recreating the StatefulSet, Kubernetes reconciled the workload based on the new spec. Even though the actual disks were already resized, Kubernetes “didn’t know” that all changes had already been applied, so it started the reconciliation process, which resulted in a full Redis cluster restart.

A distributed database/cache cluster can survive a restart sequence, but only if the restart is performed cautiously.

That brings us to the biggest amplifier in this incident.

Why it escalated: restarts were too optimistic

Our restart behavior was not defensive enough:

  • no startup and readiness probes
  • a pod being just “running” was treated as “good enough” (but in reality it still couldn’t handle the actual requests load)
  • Kubernetes proceeded to the next pod too quickly

In plain language: we had a restart process that didn’t really wait for Redis to be healthy before moving on.

At first, this still didn’t look catastrophic: the backend service complained about Redis connections, but overall service graphs didn’t immediately collapse.

Degraded Redis cluster: replicas got “stuck” to old masters

When the dust settled, the Redis cluster came up partially degraded:

  • some replica nodes couldn’t reattach properly
  • they retained stale information about which master they belonged to

At that point, recovery required manual Redis Cluster operations:

  • forgetting obsolete node identities on masters
  • resetting affected replicas
  • re-joining them to the cluster and waiting for the replication catch-up

This procedure took longer than expected. And while it was still in progress, we hit the second-stage failure.

Secondary impact: backend service overload and out-of-memory failures

While Redis nodes were rejoining and syncing, the backend service faced reconnect storms and elevated load. This turned into cascading out-of-memory failures.

Two things made this worse:

  • we initially misattributed which component was actually OOMing
  • scaling the backend service was delayed due to limited spare capacity under current resource constraints

Once the failed component was correctly identified, we stabilized the system by:

  • scaling the backend service
  • increasing memory limits for the affected component
  • …and temporarily relaxing probes to let instances enter the service immediately to smooth the recovery — otherwise it had a cascade effect: once the pod check says that a pod is “ready,” it quickly becomes overloaded with traffic and goes back to “not ready” again. This increases the load on the next pod, the situation repeats, and so on.

After that, the service gradually recovered and the clients’ traffic returned back to normal. Total downtime was appr. 50 minutes: 8:10 UTC - 09:00 UTC

Events timeline

7:00 UTC: disk size was increased in Git
7:05 UTC: the disk size was manually updated for all PVCs in the cluster
7:10 UTC: all pods have finished online disk resizing
8:10 UTC: sts orphan deletion and recreation were performed
8:10-8:15 UTC: Kubernetes quickly performed a full StatefulSet restart. At this moment problems started to appear
8:15-8:35 UTC: manual cluster restoration — replicas recreation & reallocation
8:35-8:50 UTC: dealing with application OOM-errors. Once these problems were mitigated, the service was restored with some limitations (i.e. slow responses)
10:00 UTC: temporary workarounds used for the emergency relaunch of the service were reverted. The service is fully restored and operational now

Lessons learnt

1) It wasn’t disk resize that caused the incident
The capacity fix was straightforward. The incident started with the state alignment step and the controller recreation.

2) Immutable fields push you toward risky workflows
When the system won’t let you edit a field, the workaround often involves replacement — and that replacement can trigger wide reconciliation behavior.

3) Startup probes and strict readiness are not just “nice to have”
They’re the brakes on a rolling restart. Without them, distributed systems restart in the most fragile way possible. This is the worst part of this incident — having probes would have not allowed Kubernetes to restart the next pod until the current one is really ready.

4) Recovery plans must assume slow cluster healing
Rejoins and replication catch-ups take time. Dependent services need to be designed to degrade gracefully during that window.

What are we going to do?

1) Implement missing startup probes
Simple startup probes like “do not mark the pod as ‘ready’ until the dataset is loaded” will prevent this situation from happening again.

2) Make our own “control plane” for Redis
This is a more complex thing, but it is very helpful in a situation when a restarted pod is “stuck” to an old master IP address.

3) Decrease dependency on this Redis cluster
As of now, it looks like Redis-cluster is the SPOF (single point of failure) for the whole service, so we are planning to migrate some important information to other data sources — like a separate Redis cluster for each server. We also plan to start using Kafka for some of the backend’s components.

喜歡這篇文章嗎?
9,332 9332 使用者評論
非常好!

AdGuard VPN Windows 版

安心使用任何瀏覽器或應用程式,而不用再擔心匿名性。使用 AdGuard VPN 體驗當整個世界仿佛就在你的指尖上時是什麼樣的感覺。
下載
透過下載該程式,您接受授權協定的條款
了解更多
AdGuard VPN Windows 版的主畫面,顯示 VPN 連線已啟用。
AdGuard VPN Windows 版的主畫面,顯示已中斷連線的狀態。
例外清單畫面,顯示不使用 VPN 的網站清單。
9,332 9332 使用者評論
非常好!

AdGuard VPN Mac 版

從我們的位於世界各地的 75+ 個伺服器位置一鍵選擇所需要的伺服器。安全隱藏個人數據避免受到他人的監視。
下載
透過下載該程式,您接受授權協定的條款
了解更多
AdGuard VPN Mac 版的主畫面,VPN 連線已啟用。
AdGuard VPN Mac 版的主畫面,顯示未連線狀態。
例外清單畫面,顯示不使用 VPN 的網站清單。
9,332 9332 使用者評論
非常好!

AdGuard VPN Android 版

AdGuard VPN Android 版提供數十個高速伺服器位置。隨時隨地更私密地瀏覽。內建 DNS 保護,封鎖廣告和追蹤器。
了解更多
下載
透過下載該程式,您接受授權協定的條款
Google Play
透過下載該程式,您接受授權協定的條款
AdGuard VPN Android 版的主畫面,顯示 VPN 連線已啟用。
AdGuard VPN Android 版的主畫面,顯示連線已中斷的狀態。
「位置」畫面顯示可用的最快伺服器。
例外清單畫面,顯示不使用 VPN 的網站清單。
顯示用於安裝 AdGuard VPN 之 QR 碼的畫面。
9,332 9332 使用者評論
非常好!

AdGuard VPN iOS 版

使用適用於 iOS 的 AdGuard VPN 匿名保護個人隱私並觀看您喜歡的節目。使用內建 DNS 保護封鎖廣告。
了解更多
App Store
透過下載該程式,您接受授權協定的條款
AdGuard VPN iOS 版的主畫面,顯示 VPN 連線已啟用。
AdGuard VPN iOS 版的主畫面,顯示為已中斷連線狀態。
「位置」畫面顯示可用的最快伺服器。
例外清單畫面,顯示不使用 VPN 的網站清單。
顯示用於安裝 AdGuard VPN 之 QR 碼的畫面。
9,332 9332 使用者評論
非常好!

AdGuard VPN 於 Android TV 平台

探索專為 Android TV 設計的 AdGuard VPN!享受無縫串流、加強的安全防護,以及簡單的設定過程。
下載
透過下載該程式,您接受授權協定的條款
Google Play
透過下載該程式,您接受授權協定的條款
了解更多
AdGuard VPN Android TV 版的主畫面,已啟用與芬蘭赫爾辛基伺服器的 VPN 連線。
9,332 9332 使用者評論
非常好!

AdGuard VPN Chrome 版

AdGuard VPN Edge 版

AdGuard VPN Firefox 版

AdGuard VPN Opera 版

隱藏實際位置並“飛到“世界各地、不受速度限制訪問任何內容並保持 Web 上的匿名性。
一鍵連線不同的地理位置,隱藏 IP 位址,讓網路瀏覽更加安全及隱秘。
保護隱私、隱藏真實地理位置、自己決定哪一個網站的流量要通過 VPN 傳送!
成為 Opera 瀏覽器裡的忍者:快速移動到世界任何地方,而不會被他人檢測到。
安裝
透過下載該程式,您接受授權協定的條款
了解更多
安裝
透過下載該程式,您接受授權協定的條款
了解更多
安裝
透過下載該程式,您接受授權協定的條款
了解更多
安裝
透過下載該程式,您接受授權協定的條款
了解更多
AdGuard VPN 瀏覽器擴充功能的主畫面,顯示 VPN 連線已啟用。
「位置」畫面顯示可用的最快伺服器。
9,332 9332 使用者評論
非常好!

AdGuard VPN 用於路由器

在路由器上安裝 AdGuard VPN 全面保護網路。自己決定保護時間,以及要保護的裝置
此選項僅在訂購 AdGuard VPN 後可用
具有鑰匙孔盾牌圖示的路由器。
9,332 9332 使用者評論
非常好!

AdGuard VPN Linux版

安裝用於 Linux 的最佳免費 VPN 服務,享受無縫銜接的網頁瀏覽、更高的安全性、網際網路流量加密和 DNS 防泄露保護。從多個 VPN 伺服器中進行選擇自己喜歡的伺服器位置
AdGuard VPN 的吉祥物天狗與 Linux 企鵝 Tux 一起坐在灌木叢中。
9,332 9332 使用者評論
非常好!

AdGuard VPN Apple TV 版

探索專為 Apple TV 設計的 AdGuard VPN!享受無縫串流、加強的安全防護,以及簡單的設定過程
此選項僅在訂購 AdGuard VPN 後可用
一部帶有 AdGuard VPN 標誌的 Apple TV 串流裝置。
9,332 9332 使用者評論
非常好!

AdGuard VPNXbox 版

使用 AdGuard VPN 保護 Xbox,享受順暢遊戲體驗、增強的安全性和簡單的設定
此選項僅在訂購 AdGuard VPN 後可用
一台 Microsoft Xbox 主機和控制器,附有 AdGuard VPN 標誌。
9,332 9332 使用者評論
非常好!

AdGuard VPN PS4/PS5 版

使用 AdGuard VPN 保護 PlayStation,享受無縫線上遊戲、增強的安全性和簡單的設定。從多個 VPN 伺服器中進行選擇,訪問想要的位置
此功能僅在 AdGuard VPN 高級版可用
此選項僅在訂購 AdGuard VPN 後可用
一台 Sony PlayStation 遊戲主機和控制器,上面有 AdGuard VPN 標誌。
9,332 9332 使用者評論
非常好!

AdGuard VPN Chromecast 版

在 Google TV(Chromecast 4代),或在網路路由器(Chromecast 3代)上安裝 AdGuard VPN,線上保持匿名狀態,隨時隨地訪問網頁以享受 Chromecast 的串流內容。 在 Chromecast 3代上,使用者需要 AdGuard VPN 訂閱。
此選項僅在訂購 AdGuard VPN 後可用
一部帶有鑰匙孔盾牌圖示的 Google Chromecast 串流裝置。
AdGuard VPN
下載已開始
點擊由該箭頭所指示的按鈕以開始安裝。
掃描 QR 圖碼在行動裝置上安裝 AdGuard VPN
顯示用於安裝 AdGuard VPN 之 QR 碼的畫面。