← Go Back to Professional Blog
Reducing data remediation runtime from 55 hours to 7 seconds with Aho-Corasick
Note
Because this blog post represents an actual project from my job, and this is not my employer’s official blog, I have obscured or changed certain key facts. Specifically, astute readers will notice I am vague and shifty about where datasets originated from — where they were stored and how they were retrieved, that is. That’s deliberate. Those changes do not effect the substance of this article, however.
Aho-Corasick is a classic multi-pattern string matching algorithm — like regex, but significantly faster. Unlike regex, which (depending on the implementation) may scale exponentially with input size, Aho-Corasick scales linearly. That property — linear time complexity — makes it ridiculously fast, and ideal for large-scale data remediation.
I first learned about Aho-Corasick back in 2018 but filed it away under “interesting but unnecessary for me right now.” Years later, I found myself responsible for remediating records across datasets at petabyte scale.
At the time, remediations were being performed using SQL queries embedded with regular expressions, running against an already-overloaded relational database. The approach was inefficient: most client datasets took between 30 minutes to an hour to process, and in some cases stretched as long as 55 hours. As you can probably imagine, those datasets with excessive runtimes were enormous.
Recognizing an opportunity to improve things, I began re-architecting the remediation code. Given the scale of the data and the need for runtime and memory efficiency, it quickly became clear that the shared RDBMS was a major bottleneck. Profiling revealed that regular expressions — not just SQL overhead and database performance — were the primary cause of long runtimes. As the number and length of substrings grew, performance degraded exponentially. This also proved true when using Python’s built-in re module.
In modern data engineering, large datasets are often processed using distributed systems like Apache Spark, with custom user-defined functions to apply transformation logic. Another common tool is DuckDB, which also supports UDFs and is popular for fast local analytics and ease of use.
However, DuckDB began leaking memory at scale — confirming concerns previously raised by the community. And while Spark offered better scale, efficiently sharing a large Aho-Corasick automaton between executors introduced more complexity than it solved. User-defined functions also tend to be inefficient.
To avoid shared-memory challenges in Spark and instability in DuckDB, I opted to use Python’s built-in multiprocessing library in tandem with Aho-Corasick.
Since Python multiprocessing typically involves deep memory copying between processes, which would have blown up RAM usage at scale, I used the fork start method on Linux to ensure memory sharing via copy-on-write semantics. This allowed large, immutable data structures (like the target dataset and automatons) to be reused across processes without duplication, keeping memory usage comfy.
Additionally, I took care to scope all shared data within a global cache, which avoids passing large payloads via Pool.apply_async(...) calls and minimizes serialization overhead.
The results were staggering: 55 hours → 7 seconds.
A key reason for this performance wasn’t simply the use of Aho-Corasick but how the data was shared. By loading large datasets and compiled automatons once, and forking worker processes afterward, I avoided memory duplication entirely. If I had passed these structures via arguments or used spawn, it would have resulted in prohibitive memory usage and slower compute due to unnecessary serialization and GC pressure.
Critical Concepts ¶
Before you rush to implement Aho-Corasick expecting miracles, a word of caution: Aho-Corasick + parallelization won’t yield superb performance unless your code is optimized.
To borrow from the pseudo-code further below, you’ll need to understand:
- The difference between “spawn” and “fork” memory allocation modes in Python’s multiprocessing module and how fork enables memory-efficient parallelism via copy-on-write. This matters because using spawn (the default on macOS and Windows) will fully copy large objects, causing massive memory spikes if you’re not careful. On Linux, fork allows those objects to be shared so long as they’re never mutated.
- The importance of the “tidy data” principle and general data layout — so your data can be scanned efficiently.1
- The necessity of profiling your code, early and often.2
Additionally, realize you may not need to use the multiprocessing library after all. You might be able to write a user-defined function that’s implemented in DuckDB or Spark. That decision depends primarily on the scale of your data3 and-or comfort with digging deep into Spark. To be honest, I actually recommend that you use a user-defined function in DuckDB — that is, if the scale of your data isn’t enormous. It will be less efficient than using Aho-Corasick + multiprocessing but certainly simpler.
The pseudo-code that follows accepts two pandas DataFrame objects: target and sensitive_values.
targetcontains the data that must be scanned for sensitive content and remediated sensitive_values contains the values to search for and obfuscate- The Aho-Corasick automaton does not care how sensitive values appear in target—whether as substrings or exact matches. All matches are remediated the same way.
For example:
- If
"1234"is a sensitive value, then"1234_5678"becomes"xXxX_5678" - If
"1234"appears on its own, it becomes"xXxX".
Multiple matches per record are handled without issue. Referring back to the tidy data concept: sensitive_values is represented in what I will call a “long format”.
Why does this matter so much . . . ? Because this long format allows the data to be grouped by element_name, deduplicated, and rapidly loaded into per-column automatons. Each parallel process then scans each record in each column for matches in its associated automaton.
Dependencies ¶
You’ll need to download pyahocorasick and pandas. You can use polars instead of pandas if you prefer. polars may actually make the following pseudo-code even faster, albeit marginally4. There are also Rust-based implementations available.
Pseudo-Code ¶
Tip
You can copy lines of code or permalinks by selecting the line numbers below and clicking the ellipses button. To select multiple lines, press and hold Shift (⇧) as you select the desired line range.
| |
I am being deliberately vague here because this topic could easily be a whole other blog post. ↩︎
If you want a tool that makes memory and runtime profiling incredibly easy then check out this repository I wrote. Sometimes, line-by-line profiling is too granular; rather, you need to understand how your code performs, from a memory allocation perspective, temporally. I wrote this repository for those situations exactly — but with an emphasis on simplicity and speed. ↩︎
I am not aware of any hard and fast statistics on exact thresholds for memory leakage in DuckDB so DYOR and experimentation. ↩︎
I tested this.
polarsdid decrease runtime, but not by much. This is not especially surprising since profiling revealed, as mentioned elsewhere, that regex was by far the biggest performance bottleneck (from a runtime perspective). The data layout was another major performance bottleneck (as mentioned elsewhere), but I digress. That being said, I did not record the runtime improvements whichpolarsyielded. DYOR if you prefer. Your experience may vary. ↩︎