ZYVOPMulti-Platform Sync
SeriesAI NewsWhy ZyVOPJoin Discord
LoginGet Started
ZYVOPMulti-Platform Sync

The Developer Publishing Hub. Write once, publish everywhere, and make your work citation-ready with built-in SEO, AEO, and GEO discovery support. Zero reader paywalls.

Content

  • Categories
  • Tags
  • Badges
  • Leaderboard
  • Write Article
  • Newsletter

Company

  • About Us
  • Why ZyVOP
  • Developer API & CLI
  • Author Handbook
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

© 2026 ZyVOP. Developer Publishing Hub.

Zero paywalls · Full content ownership
All systems operational
HomeBig-O Notation Explained: The Regex That Took Down Cloudflare for 27 Minutes

Big-O Notation Explained: The Regex That Took Down Cloudflare for 27 Minutes

Day 1 of the DSA Mastery Series — the concept every post after this one leans on

Arpan Singh
Arpan Singh
Senior Developer
July 20, 2026Updated September 1, 2026
8 min read
Series

DSA Mastery Series

Part 1 of 2

Prev
Next
Big-O Notation Explained: The Regex That Took Down Cloudflare for 27 Minutes
#Algorithms#datastructures#computerscience#performance#regex
👍1

At 13:42 UTC on July 2, 2019, Cloudflare's network stopped answering requests. Not slow. Down.

For 27 minutes, a huge slice of the web returned 502 errors instead of loading.

The cause wasn't a DDoS attack or a hardware failure. It was one regular expression, in one firewall rule, pushed to every server Cloudflare owns at the same time.

That regex had a property called catastrophic backtracking. Feed it the wrong kind of input, and instead of matching in microseconds, the matching time roughly doubles with every extra character.

Cloudflare's own post-mortem lays it out plainly: a WAF rule update shipped a regex that ate CPU on every core handling HTTP and HTTPS traffic, worldwide, within minutes of going live.

That single deployment is the entire reason Big-O notation is worth learning as a working developer, not just memorizing for an interview and forgetting.

Every data structure and algorithm in this series has a Big-O. Get it wrong, and code that sails through testing can take your production system down exactly like it did for Cloudflare.

What Big-O Actually Measures

Big-O is not a stopwatch. It doesn't tell you an operation takes "3 milliseconds."

It tells you how the cost of an operation grows as the input grows. That's the whole idea.

Say a function checks every item in a list once. Double the list, and the function does roughly double the work. That growth pattern, cost scaling linearly with size, is what O(n) describes.

Big-O deliberately throws away two things: the exact hardware you're running on, and small, constant-size costs. It keeps only the part that matters once your input gets big: the shape of the growth curve.

That's also why the Cloudflare regex was so dangerous. Its growth curve wasn't linear, or even quadratic. It was exponential, and exponential curves look almost flat for small inputs, then explode the moment the input crosses a threshold. That's exactly the kind of bug that passes every test and detonates in production.

Dropping Constants: Reading Big-O Like an Algebra Problem

Say you profile a function and its operation count comes out to:

3n² + 5n + 2

Big-O notation calls this function O(n²). Here's why, term by term.

Drop the lower-order terms. Once n gets large, 5n and 2 stop mattering next to 3n². At n = 1,000,000: 3n² is 3 trillion, 5n is 5 million. A rounding error by comparison.

Drop the constant multiplier. Whether it's 3n² or 30n², both describe the same shape of growth: quadratic. The multiplier affects real-world speed, which is a separate conversation from Big-O, but it doesn't change the growth curve.

So 3n² + 5n + 2 becomes O(n²), every time. This is the most common thing beginners get wrong — they see two nested loops and one extra pass, and try to write O(2n² + n). Nobody writes that. It's O(n²).

The Complexity Ladder

Here's every complexity class you'll run into in this series, best to worst, each with one operation you already use.

Notation

Name

A real example

🟢

O(1)

Constant

Reading arr[5] from an array

🟢

O(log n)

Logarithmic

Binary search on a sorted array

🟢

O(n)

Linear

Scanning a list once for a value

🟡

O(n log n)

Log-linear

Sorting with merge sort or Python's Timsort

🟠

O(n²)

Quadratic

Comparing every pair with nested loops

🔴

O(2ⁿ)

Exponential

Naive recursive Fibonacci — and Cloudflare's regex

🔴

O(n!)

Factorial

Trying every ordering of n items

Green means it scales to millions of items without blinking. Yellow still holds up at real-world scale. Orange starts hurting once n hits the thousands. Red is the zone Cloudflare's regex lived in — fine for tiny inputs, catastrophic past a threshold.

Each step down that table is a different universe of slowdown. At n = 20, O(n) and O(2ⁿ) are both fast. At n = 40, O(n) is still instant. O(2ⁿ) has over a trillion steps left to run.

Best Case, Average Case, Worst Case

Big-O by itself usually means the worst case: the input that makes an algorithm work hardest.

Linear search through an unsorted list is O(n) worst case (the value isn't there, or it's last) but O(1) best case (it's the first element checked).

Quicksort is the classic split-personality algorithm: O(n log n) on average, but O(n²) worst case if it keeps picking a bad pivot on already-sorted data. That's exactly why production sort implementations (Python's Timsort, V8's sort) don't run plain quicksort. They hedge against that worst case directly.

When someone asks "what's the Big-O of this," they almost always mean the worst case unless they say otherwise. That's the convention this series follows too.

Space Complexity: The Other Half of the Bill

Big-O also measures memory, not just time. Same rules, different resource.

An algorithm that builds a second array the same size as the input is O(n) space. One that tracks a couple of variables, regardless of input size, is O(1) space.

Recursive functions carry a space cost most beginners miss: every call sits on the call stack until it returns. A recursive function that goes n levels deep uses O(n) space on the stack alone, even without allocating a single array. That'll matter once recursion shows up properly in Day 3.

Amortized Time: Why "Append" Isn't Always O(1)

Both Python's list.append() and JavaScript's array.push() get described as O(1). That's not the whole story.

Under the hood, both languages back their dynamic arrays with a fixed-size block of memory. Fill it up, and the next append has to allocate a bigger block and copy every existing element over — an O(n) operation.

Both languages soften this the same way: they over-allocate, growing the backing array by a multiplying factor instead of one slot at a time. Resizes get rarer as the array grows, so the average cost per append, across many appends, still comes out to O(1). That average has a name: amortized O(1). It's worth knowing before dynamic arrays show up properly in Day 2.

How to Read Your Own Code's Complexity

There's a repeatable process here, simpler than it looks once you've done it a few times.

flowchart TD
    A[Look at the loops] --> B{Loops nested?}
    B -->|No, sequential| C["Add them: O(a) + O(b), keep the larger term"]
    B -->|Yes| D["Multiply them: O(a) times O(b)"]
    D --> E{Does each step cut<br/>the input in half?}
    E -->|Yes| F["O(log n) for that part"]
    E -->|No| G["O(n) for that part"]
    C --> H{Any recursive calls?}
    G --> H
    F --> H
    H -->|Yes| I["Count total calls made,<br/>not just the visible loop"]
    H -->|No| J["Done, that's your Big-O"]

Walk that against a real function:

def has_duplicate_pair(nums):
    for i in range(len(nums)):             # loop 1: n iterations
        for j in range(i + 1, len(nums)):  # loop 2: nested inside loop 1
            if nums[i] == nums[j]:
                return True
    return False

Two loops, nested. That means multiply. Loop 1 runs n times, loop 2 runs roughly n times too (shrinking a bit each pass, a detail Big-O ignores). O(n) times O(n) = O(n²).

Compare that to a function that looks similar but isn't:

def has_negative_or_over_limit(nums, limit):
    for n in nums:               # loop 1: n iterations
        if n < 0:
            return True
    for n in nums:                # loop 2: separate, not nested
        if n > limit:
            return True
    return False

Two loops again, but sequential, not nested. Add instead of multiply: O(n) + O(n), the constant drops, and it's O(n). Seeing "two loops" and assuming O(n²) automatically is one of the most common mistakes in code review.

The Benchmarks: What This Looks Like on Real Hardware

Theory is one thing. Here's what these growth curves look like measured for real — Python 3.12 and Node.js 22, same machine, same inputs, timed with timeit and process.hrtime.bigint().

O(1): array index access

n

Python (ns/op)

Node.js (ns/op)

1,000

43.3

14.2

10,000

31.3

9.5

100,000

32.1

10.4

1,000,000

30.8

27.2

Flat, as expected. The time doesn't track n at all. The small jump at 1,000,000 in the Node.js column isn't a Big-O violation; that's the array outgrowing CPU cache, a real hardware effect Big-O doesn't model. Big-O describes algorithmic growth, not what the cache hierarchy underneath it is doing.

O(log n): binary search

n

Python bisect (ns/op)

Node.js (ns/op)

1,000

114.8

35.8

10,000

121.7

24.9

100,000

156.8

25.4

1,000,000

159.0

28.6

The input grew 1,000x. The time barely moved. That's the entire pitch for logarithmic algorithms in one table.

O(n): linear search, worst case (target never present)

n

Python (µs/op)

Node.js (µs/op)

1,000

6.62

1.11

10,000

59.49

10.14

100,000

616.47

102.58

1,000,000

6,060.19

1,514.06

10x the input, roughly 10x the time, in both languages. Textbook linear.

O(n log n): sorting (Python's Timsort, V8's sort)

n

Python (ms/op)

Node.js (ms/op)

1,000

0.083

0.202

10,000

1.262

2.057

100,000

18.386

26.654

1,000,000

260.294

326.918

Growing faster than linear, much slower than quadratic — the middle ground n log n describes. Don't read too much into Python edging out Node.js here; it's one machine, one run, and the gap is small enough that engine warm-up explains most of it, not "Python's sort is better."

O(n²): checking every pair for duplicates, nested loops

n

Python (ms/op)

Node.js (ms/op)

500

3.25

0.09

1,000

13.82

0.34

2,000

53.97

1.32

4,000

219.10

5.29

Double n, and the time roughly quadruples, in both languages, at every step. That's the signature of O(n²): a pattern you can watch happen in real numbers.

O(2ⁿ) vs O(n): naive vs. memoized Fibonacci

n

Naive Python (ms)

Memoized Python (µs)

Naive Node.js (ms)

Memoized Node.js (µs)

20

0.663

7.63

0.091

3.61

25

8.613

6.01

0.706

4.31

30

78.953

6.84

7.700

5.18

32

206.629

7.15

20.428

5.11

This is the table that matters most in the whole post. Going from n=30 to n=32 — adding two — makes the naive version take roughly 2.6x longer, in both languages independently. That 2.6x isn't a coincidence: it's the golden ratio squared, baked into how Fibonacci's recursion tree branches.

The memoized version barely moves across the entire range. One dictionary, or one Map, turns an O(2ⁿ) function into an O(n) function. The benchmark shows exactly what that's worth: milliseconds versus microseconds.

The benchmark scripts used to generate the numbers listed above have been uploaded to GitHub for reference.

def fib_naive(n):
    if n <= 1:
        return n
    return fib_naive(n - 1) + fib_naive(n - 2)


def fib_memo(n, cache={}):
    if n <= 1:
        return n
    if n in cache:
        return cache[n]
    cache[n] = fib_memo(n - 1, cache) + fib_memo(n - 2, cache)
    return cache[n]

Where the Regex Went Wrong

Back to Cloudflare, with the vocabulary to actually explain it now.

A backtracking regex engine, faced with an ambiguous pattern, can end up trying every possible way to match it — the regex equivalent of the naive Fibonacci function above, except n is the length of the input string, and there's no memoization built in by default.

Small inputs, like most real traffic, never trigger the worst case. That's exactly why the rule passed testing.

One request with the right shape of pathological input turned a normally instant regex into an O(2ⁿ) computation, run on every CPU core, on every server, at the same time. That's the outage in one sentence.

What's Next

Day 2 starts with arrays. There's enough ground there to split it: fundamentals and core operations first, then patterns like two-pointer and sliding window as their own follow-up. Linked lists get the same treatment once we reach them: singly and doubly linked first, then the interview-heavy patterns (reversal, cycle detection, merging) as a separate post.

Every post from here on states the time and space complexity of every operation it covers. This is the post that vocabulary comes from.

Series

DSA Mastery Series

Part 1 of 2

Prev
Next

Comments (0)

Login to post a comment.

Arpan Singh
Arpan Singh

Passionate developer sharing knowledge about modern web technologies and best practices.

Subscribe to Arpan Singh's Newsletter

Direct email dispatches when new stories are published. Zero algorithms.

Big-O Notation Explained: The Regex That Took Down Cloudflare for 27 Minutes

More from Arpan Singh

View profile

1024 Bytes of C Can Fake Python. Here's Exactly Where the Illusion Cracks.

Austin Henley squeezed a Python-like interpreter into 1,024 bytes of C. We verified the byte count, benchmarked it against CPython at scale, and found four silent bugs, from reserved variable names to a chained-comparison parser glitch, that never made his feature list.

10 minSep 7

ThreeUI: Getting-Started Walkthrough & Architecture Review

A hands-on install guide and architecture review of ThreeUI, Meng To's Three.js/WebGL component catalog — covering the Community/Pro sync pipeline, automated releases, licensing, and the specific claims that couldn't be independently verified against public sources.

5 minSep 5

TypeScript in 2026: What Changed, and the Patterns Worth Adopting

TypeScript 7.0 shipped a Go-native compiler that's up to 12x faster — but not all tooling has caught up yet. Here's what to adopt now, what to hold off on, and nine practical patterns worth adding to your code today.

6 minAug 27

Arrays Explained from Memory to Big O (With Real Benchmarks)

A common array bug that silently deletes the wrong items, and what it reveals: how arrays actually work in memory, why insert/delete cost what they cost, and real benchmarks in Python and Node.js to prove it.

7 minJul 24

TypeScript 7 Just Shipped: I Checked Five Claims Going Around Against Microsoft's Own Numbers

TypeScript 7 is out, and the talking points have already calcified: 10x faster, half the memory, same compiler, just upgrade. Some of that's exactly right. Some doesn't survive contact with Microsoft's own release notes — including the one thing nobody mentioned until their build broke.

9 minJul 9