AIニュース最前線
最新ニュースAI日報Hacker日報週報動画AIツールトレンド企業

AIニュース最前線

世界中のAI最新情報を日本語で毎時更新

最新ニュース日報トレンド企業プレミアムRSS
© 2026 ainew.jp特定商取引法に基づく表記
ニュース一覧元記事を開く
Cloudflare Blog·2026年6月12日 22:00·約16分で読める

セキュリティインサイトのスケーリング:グローバルスキャン能力を10倍に向上させた方法

#セキュリティアーキテクチャ#Apache Kafka#マイクロサービス#スケーラビリティ#Cloudflare
TL;DR

Cloudflare は、セキュリティリスクの検出遅延とスキャン漏れを解消するため、Kafka のパーティショニング戦略やマイクロサービスのアーキテクチャを見直し、スキャン処理能力を10倍に拡張する技術的課題解決を行いました。

AI深層分析2026年6月12日 23:03
3
注目/ 5段階
深度40%
4
関連度30%
2
実用性20%
5
革新性10%
3

キーポイント

1

既存システムのボトルネックとリスク

週次または隔週の頻度でのスキャンや無料プランのオプトイン方式により、セキュリティ設定ミスが最大2週間検出されないリスクが生じており、自動化攻撃の加速に伴いこの問題は深刻化していました。

2

10倍のスループット向上目標

すべてのアカウントで自動スキャンを有効にし頻度を倍増させるため、毎秒 10 スキャンから 100 スキャンへの処理能力拡大が必要となり、既存の API タイムアウトやプロセスクラッシュという課題に直面しました。

3

Kafka のアーキテクチャ特性と制約

Apache Kafka はキューではなくパーティション化されたイベントストリームであり、1 つのパーティション内でメッセージ順序保証が必要なため、1 つのコンシューマーグループにアクティブなコンシューマーは 1 つしか置けないという制約があります。

4

スケーラビリティへの対応戦略

処理が遅いメッセージが次のメッセージの消費をブロックする問題を解決し、マイクロサービス(Go)と Postgres データベースを用いたアーキテクチャを最適化することで、大規模なイベントバックログの解消を図りました。

5

バッチ処理と並行実行の導入

メッセージをバッチで消費し、各メッセージを別々のゴルーチンで処理することで並列化を実現しました。

6

ヘッド・オブ・ライン・ブロッキングの回避

処理時間のばらつきによる遅延を防ぐため、チェックグループとチェッカーを「高速レーン」と「低速レーン」に分割し、低速メッセージは別リソースで処理する仕組みを導入しました。

7

データベースクエリの最適化

大量のインサイト書き込み時に発生していた1件ごとのDB往復を解消するため、COPY 命令によるバッチ挿入を検討しましたが、システムテーブルの肥大化を避けるため別のアプローチを採用しました。

影響分析・編集コメントを表示

影響分析

この記事は、大規模なクラウドセキュリティプラットフォームにおいて、イベント駆動型アーキテクチャの限界とそれを突破する具体的な手法を示しており、DevSecOps や SRE の現場におけるスケーリング戦略の参考事例として極めて重要です。特に Kafka のパーティショニング特性を深く理解した上でシステムを再設計した点は、同様の高負荷分散処理が必要な組織にとって実用的な知見を提供します。

編集コメント

セキュリティ対策の自動化において、単に頻度を上げるだけでなく、背後にあるイベント処理基盤のスケーラビリティをどう設計するかが成否を分けることを示した実例です。

Security Insights は、すべての Cloudflare アカウントに対して実行可能なセキュリティ推奨事項を提供します。これらのインサイトを見つけるために、私たちはすべてのアカウント、ゾーン、DNS レコードに対して定期的なスキャンを実行し、潜在的なセキュリティリスクや設定ミスを調査しています。

しかし、2 つの主要な課題が浮上しました。第一に、私たちのスキャン頻度が低すぎました。スキャンは週に 1 回または 2 回しか実施されておらず、その結果、新たに導入されたセキュリティリスクが最大で 2 週間検出されないままである可能性があります。第二に、多くの無料プランアカウントでは自動スキャンがオプトイン方式でした – つまり、多くのアカウントは一切スキャンされませんでした。

頻度が低い、あるいは存在しないスキャンのリスクは高まっています。自動化された攻撃が加速するにつれて、セキュリティ設定ミスを検出するための時間的余裕は縮小しています。すべての顧客に対してこれらの問題を発見できていることを確認することは、誰もがより良いインターネットを構築するという私たちの目標にとって不可欠です。

スキャン頻度を高め、すべてのアカウントで自動スキャンを有効化するためには、平均してスキャン処理能力を約 10 倍に増やす必要があると計算しました – 1 秒あたり 10 スキャンから 100 スキャントへ。しかし、私たちのシステムはすでに負荷に苦しんでいました:数百万のイベントがバックログを埋め尽くし処理待ちの状態でした;API は頻繁にタイムアウトしていました;プロセスはクラッシュしていました。私たちはシステムを修正する必要があり、それをスケールさせる必要がありました。

これは、Security Insights のスキャン処理量を 10 倍以上に増強し、数百万人の顧客にセキュリティインサイトの提供を可能にし、全顧客のスキャン頻度を倍にした物語です。これらの改善をどのように達成したかについては、以下をご覧ください。

セキュリティインサートを得るためのスキャン方法

高レベルでは、自動セキュリティスキャンはスケジューラによってトリガーされます。アカウントまたはゾーンがスキャンの期限を迎えると、スケジューラは Apache Kafka(オープンソースの分散イベントストリーミングプラットフォーム)にメッセージ(または複数のメッセージ)を公開します。これらのメッセージは、特定の資産や構成を検査する専用の Go マイクロサービスである多数のチェッカーに配信されます。

各メッセージに対して、各チェッカーはその結果(発見されたセキュリティインサイト)を内部 API に送信し、API がこれを Postgres データベースに永続化します。

image
image

スケーラビリティの確保

Kafka のスケール

Apache Kafka は厳密にはキューではなく、パーティション化されたイベントストリームです(ただし最近になってキューのセマンティクスも獲得しました)。1 つのパーティション内では、メッセージは順序通りに消費され処理される必要があります。これは、メッセージが順序通りに消費されることはあっても並列に処理される一般的なキューとは異なります。その結果、コンシューマーグループ内では 1 つのパーティションあたりアクティブなコンシューマーを 1 つしか持つことができません。

これにより、私たちに生じる影響は 2 つあります。

  • 処理に時間がかかるメッセージが、コンシューマーが次のメッセージに進むのをブロックする
  • 各チェッカーについては、パーティション数と同じ数のコンシューマーしか持てない(各チェッカーには独自のコンシューマーグループが存在する)
image
image

パーティション数を増やすことでスケーリングを試みることもできました。しかし、これにより多くの他のサービスと共有されている Kafka ブローカー自体のリソース使用量が増加してしまいます。そのため、これは最終手段として温存し、まずはコードとアーキテクチャの改善を目指すことにしました。

並列処理の導入

メッセージは順序通りにしか消費できませんが、同時に複数のメッセージを消費することに何ら制限はありません。

チェック処理をバッチ方式に変更し、各メッセージを別々のゴルーチンで処理するようにしました。その代償として、プロセスがバッチの途中まででクラッシュした場合に再処理する作業量が増え、メモリ使用量がわずかに増加します。しかし、私たちのケースではこれらはいずれも許容範囲内でした。

ヘッド・オブ・ライン・ブロッキングの回避

一部のチェックャーが処理するメッセージの中には、他のメッセージに比べて非常に時間がかかるものがあります。例えば、あるアカウント/ゾーンは別のアカウント/ゾーンよりもはるかに多くのアセットを持っている場合があります。最悪の場合、これらのメッセージの処理には平均的なケースである数秒や数ミリ秒と比較して、数分あるいは数時間を要することがあります。

私たちは非常にシンプルなアプローチを採用しました:コンシューマーグループとチェックャーを「スローレーン」と「ファストレーン」の 2 つに分割することです。メッセージが処理に時間がかかるかどうかなどを素早く判断できます。「ファストレーン」のチェックャーが遅いメッセージを検出すると、それをスキップします。

imageimage

これにより問題が解決しました:遅いメッセージには専用のリソースと時間が確保され、最小限の遅延で処理できるようになり、速いメッセージは通常の高速ペースで進行できるようになりました。

データベースクエリの最適化

発見したすべてのインサイトは、Postgres データベースに書き込まれます。これは、チェックャーがインサイトのリストを指定して呼び出す単一の API エンドポイントによって処理されます。実装は以下のようになりました。

for _, issue := range issues {

_, err = tx.Exec(ctx, INSERT INTO table ... VALUES ($1, $2, ...) ON CONFLICT DO UPDATE ..., ...)

if err != nil {

return err

}

}

鋭い読者ならお気づきでしょうが、大量のインサイトに対してこのコードは、各インサイトごとにデータベースへの往復(ラウンドトリップ)を実行しています。最大観測サイズが 500,000 の場合、これは単一の API コール内で 50 万回の往復、クエリ、トランザクションを意味します。

当初は、Postgres におけるバッチ挿入のゴールドスタンダードである、一時テーブルへの COPY を試みました。しかし、このアプローチが Postgres のシステムテーブルに肥大化(ブロート)を引き起こすことが判明しました。

そこで私たちはハイブリッドなアプローチを採用することにしました。

問題数が閾値未満の場合は UNNEST を使用し、

問題数が閾値を超えた場合は COPY を使用する。

これにより、両方の利点を享受できる結果となりました。つまり、大量のインセットに対しては秒単位で比較的高速に挿入でき、少量のインサイトに対してもミリ秒単位でさらに高速に挿入できるようになりました。

API タイムアウトの調査

スケーリングを試みる中で、内部 API でいくつかの奇妙な挙動が観察されました。

多数のリクエストがクライアント側のタイムアウトを引き起こしていた

多くのチェックャーが処理時間の 20〜90% を単一の API コールに費やしていた

大量のスキャンをトリガーすると、スループットは当初高い状態から徐々に低下し始めます。

これらすべての問題には同じ根本原因がありました:レイテンシです。

私たちの主要なデータベースはオレゴン州ポートランドに位置しています。一方、API はポートランドとアムステルダムの両方でアクティブ・アクティブ構成で稼働していました。光速であっても、ポートランドとアムステルダムの間の往復遅延(レイテンシ)は 50 ミリ秒になります。

このレイテンシの結果、アムステルダムにある API インスタンスからのデータベースクエリには非常に時間がかかり、クライアント側の接続プールから保持された接続が解放されませんでした。API に対して大量のリクエストを送信していたため、接続プールはすぐに枯渇し、空き接続を待つ間にタイムアウトが発生しました。ポートランドでの平均的な API コール完了時間は 10 ミリ秒でしたが、アムステルダムではほぼ 3 秒にも達していました!

しかし、メッセージのスループットが低下した理由は何か?各チェッカープロセスには、消費する Kafka ストリームのパーティションのセットが割り当てられています。私たちの API はロードバランシングされています。プロセスのライフサイクルを通じて接続を保持しているため、一部のプロセスはアムステルダムの API への接続を持ち、他のプロセスはポートランドの API への接続を持っていました。ポートランドにリンクされたパーティションは迅速に処理されましたが、アムステルダム方面のプロセスによって消費されるパーティションは遅れをとっていました:

image
image

単一のコンシューマーグループ内で処理待ちとなっているメッセージ数であるKafkaラグ(Kafka lag)を、パーティションごとに示したものです。この場合、パーティションは合計30個あります。そのうちちょうど15個のパーティションが、遅延していることが確認できます(これは、約03/10 03:00よりも後にゼロに達するかそれに近づくラインです)。これは、ロードバランサーがトラフィックをAPIエンドポイント間で均等に分割するためです。

これは簡単な修正でした:APIをアクティブ・パッシブ構成に変更し、アクティブなAPIがプライマリデータベースに従うようにしました。その結果、翌朝にはレイテンシの問題はすべて解消されました。

スケジューラーの再考

Kafkaのスケーリングを行い、データベースクエリの最適化を施し、APIも修正しました。しかし、依然として問題が残っていました:スキャンが時間軸上でほぼ均等に分散されていることを確実にする必要があるのです。Kafkaトピックは時間ベースの保持ポリシーを採用しているため、すべてのスキャンを同時にキューに積むことは現実的ではありません。その場合、スキャンがKafka内に蓄積され、処理される前に最終的に削除されてしまうからです。

当社のスケジューラーは、スキャンを均等に分散させるのが不得意でした。特定の時刻にトリガーされるスキャン数はスパイク状であり、予測不可能でした。週の間のある時点では、数百万件のスキャンが数分ごとに連続してトリガーされていました。いったい何が起きていたのでしょうか?

スケジューラーは固定された再帰期間にスキャンをトリガーします。擬似コードでは、スケジューラーは以下のようになりました:

Loop forever:

Find accounts where last_scheduled_at + scanning frequency <= now

For each account:

Trigger scan for account

Trigger scan for all zones in the account

Update last_scheduled_at = now

私たちはすぐに、データベース内の多数のアカウントで last_scheduled_at が類似していることに気づきました。これが不均衡の一部の原因となっていました。

しかし、スキャン頻度を完璧に均等に分布させたとしても、スキャン頻度を上げればこの問題はさらに悪化します。例えば、スキャン頻度を 15 日ごとから 7 日ごとに短縮すると、アカウントの 53% が突然スキャン対象となることになります。

このロジックにはさらなる問題がありました。一部のアカウントは非常に多くのゾーンを持っています。これらのアカウントがスケジュールされると、すべてのゾーンに対して一連のスキャンがトリガーされ、Kafka パーティション (Kafka partitions) を飽和させ、より小さなアカウントのスキャンに遅延を引き起こしていました。

これらの問題を解決するために、私たちは 3 つの重要な変更を行いました:

  1. アカウントとは独立してゾーンをスケジュールする:各ゾーンは独自の last_scheduled_at フィールドを持ちます。
  2. 既存のアカウントとゾーンの last_scheduled_at 時間をランダム化する。
  3. スキャンスケジューリングに適応型レート制限 (adaptive rate limiting) を導入する。

大規模アカウントの問題を解決するための明白な方法は、ゾーンを独立してスケジュールすることでした。last_scheduled_at 時刻をランダム化し(このプロセス中にスキャンが遅延しないように確保する)ことで、データベース内の既存の偏りを修正できました。

適応型レート制限は、やや興味深いものです。レート制限により、スキャン頻度を変更した際に発生するスキャンの急増という問題を解決できます。例えば、スキャン頻度を 7 日ごとにすることにし、5000 万件のアカウントがある場合、約 83 スキャン/秒のレート制限を設定すれば、それらが 7 日間にわたって均等に分散されることを保証できます。

しかし、さらに 1000 万件のアカウントを追加したらどうなるでしょうか?その場合、このレート制限では全アカウントをスキャンするのに 8 日かかることになります。ここで適応型の要素が重要になります:レート制限は、保有するアカウント数とゾーンの総数、およびスキャン頻度に基づいて非同期で半時間ごとに再計算されます。これにより、数千件あるいは数百万件の追加のアカウントやゾーンを導入しても、スキャンを予定通りに継続して実行できます。

func computeRate(free, pro, biz, ent int64) rate.Limit {

r := float64(free)/freeScanInterval.Seconds() +

float64(pro)/proScanInterval.Seconds() +

float64(biz)/bizScanInterval.Seconds() +

float64(ent)/entScanInterval.Seconds()

// 0 カウントに対するガード。常に毎秒少なくとも 1 スケジュールのスキャンを確保したい。

if r < 1 {

r = 1

}

⟦CODE_0⟧

// '完璧' な値を超えてレート制限を引き上げ、ダウンタイムや負荷の急増に備えたバッファを確保する

// r *= rateLimitBufferFactor

return rate.Limit(r)

}

現在の状況

imageimage

これらの修正により、チェックャーあたりの 7 日間移動平均スループットは時間経過とともに 10 倍以上に向上しました。

これらの改善以前、私たちは 1 秒あたり約 10 スキャンを実行していました。この数値と目標である 1 秒あたり 100 スキャンとの間のギャップは非常に大きく見えました。問題解決のためにリソースを投入する、Kafka トピックにパーティションを追加する、あるいはアーキテクチャそのものを根本から作り変えるといった議論もなされました。

しかし、私たちの修正が劇的な違いを生みました。現在、Security Insights はピーク時のスケジューリング時に 1 秒あたり 120 スキャンを超えるスループットを維持しており、10 倍の改善目標をすでに上回っています。内部 API のタイムアウトは解消され、Kafka のラグ(遅延)メトリクスもはるかに健全な状態になっています。これらのスケーラビリティ向上により、すべての無料アカウントとゾーンで自動スキャン機能を有効化し、全顧客のスキャン頻度を増加させることが可能になりました:

無料プラン:7 日ごと

Pro および Business プラン:3 日ごと

Enterprise プラン:毎日

システムの安定性が向上したことで、以前は作成に制約があった新機能の開発に自信を持つことができました。粒度の細かいオンデマンドスキャンを実行する機能を追加しました。これにより、Cloudflare アカウント、ゾーン、インサイト、またはインサイトタイプを手動で再スキャンできるようになりました。

image
image

Cloudflare ダッシュボードのセキュリティ概要ページから、粒度の細かいオンデマンドスキャンを開始する様子

私たちが学んだ教訓は、何かを捨ててしまう前に既存のシステムを深く理解することが極めて重要だということです。コード、SQL クエリ、ログ、そして何よりもメトリクス(特にメトリクス!)を注意深く分析したおかげで、ポッドやパーティションを追加するだけでは実現できない方法でキャパシティを増やすことができました。前提条件に疑問を投げかけ、変な見た目をするメトリクスを掘り下げ、簡単な近道(例えば API のクライアントサイドタイムアウトの延長など)に頼らない姿勢を貫いた結果、より安定し回復力のあるシステムを構築することができました。

問題に対してリソースを追加投入することが解決策になる場合もありますが、Cloudflare では問題をエンジニアリングによって解決すること信じています。

セキュリティインサイトスキャンは、すべての Cloudflare プランでデフォルトで有効になっています。今日すぐに Cloudflare ダッシュボードにログインして、セキュリティインサイトをレビューおよび管理してください。

原文を表示

Security Insights provides actionable security recommendations for every Cloudflare account. To find these insights, we perform regular scans for all accounts, zones, and DNS records, looking for potential security risks and misconfigurations.

However, two key issues emerged. First, our scans were too infrequent. Scans were only being performed every week or two, and therefore newly introduced security risks could remain undetected for up to two weeks. Second, automatic scanning was opt-in for many free plan accounts – meaning lots of accounts weren’t being scanned at all.

The risks of infrequent or nonexistent scans are rising: as automated attacks accelerate, the window for detecting security misconfigurations is shrinking. Making sure that we’re finding these issues for all of our customers is crucial to our aim of building a better Internet for everyone.

We calculated that to increase our scanning frequencies and enable automatic scanning for all accounts, we would need to increase our scanning throughput by around 10x on average – from 10 scans per second to 100 per second. But our system was already struggling with its load: millions of events were filling up our backlog waiting to be processed; our API was frequently timing out; our processes were crashing. We needed to fix our system, and we needed to make it scale.

This is the story of how we increased scanning throughput for Security Insights by more than 10x, enabled security insights for millions of customers, and doubled our scanning frequency for all customers. Read on to find out how we achieved these improvements.

How we scan for security insights

At a high level, our automatic security scans are triggered by a scheduler. When an account or zone is due for a scan, the scheduler publishes a message (or messages) to Apache Kafka, an open-source distributed event streaming platform. These messages fan out to a number of checkers: specialized Go microservices that scan specific assets or configurations.

For every message, each checker sends its results (the security insights that it found) to our internal API, which then persists these in a Postgres database.

imageimage

Making it scale

Scaling Kafka

Apache Kafka is not strictly a queue: it is a partitioned event stream (though recently gained queue semantics). Within a partition, messages must be consumed and processed in order. This differs from typical queues where messages may be consumed in order but are processed out-of-order. As a result, we can only have one active consumer per partition within a consumer group.

This has two consequences for us:

Messages that are slow to process block the consumer from progressing to the next message

For each checker, we can only have as many consumers as there are partitions (each checker has its own consumer group)

imageimage

We could have tried to scale by adding more partitions. However, this would have increased resource usage for the Kafka broker itself, which is shared by many other services. We reserved this as a last resort, aiming to improve our code and architecture first.

Introducing parallel processing

Although we can only consume messages in order, there is nothing stopping us from consuming multiple messages at once.

We changed our checkers to consume messages in batches, processing each message in a separate goroutine. The trade-offs are that we’d have more work to re-do if our process crashed midway through a batch, and our memory usage would be slightly increased. In our case, these were both acceptable.

Avoiding head-of-line blocking

Some messages processed by a few of our checkers take much longer to process than others. For example, one account/zone may have far more assets than another. In the worst case, these messages can take minutes or hours to process compared to the average case of seconds or milliseconds.

We opted for a very simple approach: splitting our consumer groups and checkers in two – the ‘slow lane’ and the ‘fast lane’. We could determine quickly whether a message would be slow or fast to process. If the ‘fast lane’ checker encounters a slow message, it skips it.

imageimage

This solved the problem: slow messages had the dedicated resources and time to be processed with minimal delay, and fast messages were able to proceed at their regular fast pace.

Optimizing our database queries

Every insight we find gets written to our Postgres database. This is handled by a single API endpoint that our checkers invoke with a list of insights. The implementation looked like this:

for _, issue := range issues {

_, err = tx.Exec(ctx, INSERT INTO table ... VALUES ($1, $2, ...) ON CONFLICT DO UPDATE ..., ...)

if err != nil {

return err

}

}

The astute reader will notice that for large sets of insights, this code makes a round trip to the database per insight. With a maximum observed size of 500,000, this was half a million round trips, queries, and transactions in a single API call.

We initially tried the gold standard for bulk inserts in Postgres: COPY into a temporary table. However, we found that this approach led to bloat in the Postgres system tables.

We settled on a hybrid approach:

Using UNNEST when the number of issues was below a threshold

Using COPY when the number of issues exceeded this threshold

This provided the best of both worlds: reasonably fast inserts for huge sets of insights (seconds), and even faster inserts (milliseconds) for small sets of insights.

Investigating our API timeouts

We noticed several strange behaviours in our internal API as we tried to scale:

A large number of requests were triggering client-side timeouts

Many checkers were spending 20-90% of their processing time on a single API call

When triggering a large volume of scans, our throughput would start high and deteriorate

All of these problems had the same root cause: latency.

Our primary database is located in Portland, Oregon. Our API, however, was running active-active in both Portland and Amsterdam. Even at the speed of light, the round-trip latency between Portland and Amsterdam would be 50 milliseconds.

As a result of this latency, database queries from the Amsterdam API instance took much longer, holding connections from our client-side connection pool open. With the large volume of requests that we were making to the API, the connection pool was quickly becoming exhausted, leading to timeouts waiting for a free connection. Our average API call completed in 10 ms in Portland, but almost 3 seconds in Amsterdam!

But why the drop in message throughput? Each checker process gets assigned a set of partitions of the Kafka stream to consume. Our API is load-balanced. Since we hold the connection open throughout the life of the process, some processes had a connection to the Amsterdam API, and others had a connection to the Portland API. The partitions linked to Portland were processed quickly, but the ones consumed by the Amsterdam-bound processes were lagging behind:

imageimage

Kafka lag (number of messages waiting to be processed within a single consumer group) by partition for one of our checkers. Note that we have 30 partitions in this case. Exactly 15 partitions can be seen lagging behind (the lines that reach or approach zero later than around 03/10 03:00). This is because the load balancer splits traffic evenly between our API endpoints.

This was a simple fix: we switched our API to active-passive, ensuring the active API followed our primary database. Our latency problems disappeared overnight.

Rethinking the scheduler

We’d scaled Kafka. We’d optimised our database queries. We’d fixed our API. However, we still had a problem: we needed to be sure our scans would be roughly uniformly distributed in time. It wasn’t feasible to queue all of our scans at the same time, as our Kafka topic uses a time-based retention policy: the scans would pile up in Kafka, and eventually be deleted before they could be processed.

Our scheduler was not good at uniformly distributing our scans. The number of scans that would be triggered at a given time was spiky and unpredictable. At certain points throughout the week, hundreds of thousands of scans would be triggered within minutes of each other. What was going on?

The scheduler triggers scans on fixed recurring periods. In pseudocode, the scheduler looked like this:

Loop forever:

Find accounts where last_scheduled_at + scanning frequency <= now

For each account:

Trigger scan for account

Trigger scan for all zones in the account

Update last_scheduled_at = now

We quickly noticed that last_scheduled_at was similar for a large number of accounts in our database, which was responsible for some of this unevenness.

However, even with perfectly even distribution, increasing our scanning frequency would have compounded this problem. For example, changing the scanning frequency from every 15 days to every seven days would mean 53% of accounts would suddenly be due for a scan.

There was a further problem with this logic. Some accounts have a very large number of zones. When these accounts were scheduled, there was a cascade of scans for all of their zones. This was saturating our Kafka partitions and leading to delays for scans of much smaller accounts.

To fix these problems, we made three key changes:

Schedule zones independently of accounts: each zone gets its own last_scheduled_at field.

Randomize the last_scheduled_at time for existing accounts and zones.

Introduce adaptive rate limiting for scan scheduling.

Scheduling zones independently was an obvious way to solve the problem of large accounts. Randomizing the last_scheduled_at time (and ensuring that no scans were delayed during this process) allowed us to fix the existing unevenness in our database.

Adaptive rate limiting is slightly more interesting. Rate limiting would allow us to solve the problem of a spike in scans when we change scanning frequencies. For example, if we wanted to increase our scanning frequency to every 7 days, and we had 50 million accounts, then a rate limit of ~83 scans/second would ensure that they were spread out evenly across 7 days.

But what if we added 10 million more accounts? Then, this rate limit would force us to take 8 days to scan all of these accounts. This is where the adaptive part comes in: the rate limit is asynchronously recalculated every half-hour based on the total number of accounts and zones we have, and our scanning frequencies. This ensures we continue scanning on time even if we onboard thousands or millions more accounts and zones.

func computeRate(free, pro, biz, ent int64) rate.Limit {

r := float64(free)/freeScanInterval.Seconds() +

float64(pro)/proScanInterval.Seconds() +

float64(biz)/bizScanInterval.Seconds() +

float64(ent)/entScanInterval.Seconds()

// Guard against zero counts. We always want to schedule at least one scan per second.

if r < 1 {

r = 1

}

// Increase rate limit beyond the 'perfect' value, to have a buffer in case of any downtime

// or spikes in load.

r *= rateLimitBufferFactor

return rate.Limit(r)

}

Where we stand today

imageimage

With these fixes, our 7-day moving average throughput per checker over time rose by more than 10x.

Before these improvements, we were executing around 10 scans per second. The gap between this and our target throughput of 100 scans per second seemed vast. We discussed throwing more resources at the problem, throwing more partitions at our Kafka topic – even throwing out our entire architecture.

But our fixes made all the difference. Today, Security Insights sustains over 120 scans per second during peak scheduling, exceeding our 10x improvement goal. Our internal API is no longer timing out, and our Kafka lag metrics look much healthier. These scalability improvements have allowed us to turn on automatic scanning for all free accounts and zones and increase the scanning frequency for all customers:

Free: every 7 days

Pro and Business: every 3 days

Enterprise: daily

The improved system stability has given us confidence to build new features that we were previously constrained from creating. We’ve added the ability to perform granular on-demand scans. You can now manually re-scan a Cloudflare account, zone, insight, or insight type.

imageimage

Starting a granular on-demand scan from the Security Overview page in the Cloudflare dashboard

The lesson we learned is that it’s crucial to deeply understand the existing system before throwing anything away. By looking closely at our code, SQL queries, logs, and metrics (especially metrics!), we were able to increase our capacity without simply adding more pods or partitions. By questioning our assumptions, digging into weird-looking metrics, and refusing to take the easy shortcuts (such as increasing API client-side timeouts), we built a more stable and resilient system.

Throwing more resources at the problem might sometimes be the answer, but at Cloudflare, we believe in engineering our way out of problems.

Security Insights scans are enabled by default on all Cloudflare plans. Log in to the Cloudflare dashboard today to review and manage your security insights.

この記事をシェア

関連記事

InfoQ2026年4月3日 18:00

100以上のサービスを停止させずにデータベースシーケンスを大規模に置き換える

Saumya Tyagiが、多数のサービスに影響を与えずにデータベースシーケンスを大規模に置き換える方法について説明している。

Cloudflare Blog★42026年2月13日 23:00

ecdysisによる古いコードの脱皮:CloudflareのRustサービスにおける優雅な再起動

ecdysisはRustライブラリで、ネットワークサービスのダウンタイムゼロアップグレードを実現。Cloudflareで5年間使用後、オープンソース化。

Mercari Engineering★32026年6月18日 11:00

メルペイのキャンペーン基盤をルールベース汎用システムへ再構築し、Otoku Revolution を実現したまでの話

メルカリのGrowth Platform Teamが、メルペイのポイント還元キャンペーン基盤「Santa」をルールベースの汎用システムに書き直し、大規模な機能刷新(Otoku Revolution)を実現した経緯について述べています。

今日のまとめ

AI日報で今日の重要ニュースをまとめ読み

ニュース一覧に戻る元記事を読む