Skip to content
← Back

minimatch-fast: How to speed up glob matching up to 26x without changing a single line of code

7 min read
TypeScriptNPMGlobPerformanceSecurityNode.js

Minimatch is the most widely used glob matcher in the Node.js ecosystem. It also has a denial of service vulnerability since 2022 and is slow on complex patterns. This package is a drop-in replacement that uses picomatch internally, passes all 355 original minimatch tests, and speeds up matching between 1.3x and 26x depending on the pattern. One npm install and your globs fly.

You have a Node.js project that uses minimatch to filter files. It works fine with simple patterns like *.js or src/**/*.ts. But one day you add a pattern with braces like {src,lib}/**/*.{js,ts,jsx,tsx} and notice the build takes longer than expected. You investigate and discover that minimatch converts that pattern into a regular expression that JavaScript’s engine executes with exponential backtracking.

The problem isn’t just performance. Minimatch has CVE-2022-3517, a denial of service vulnerability through regular expressions. An attacker can send a malicious pattern that freezes your server for minutes. The vulnerability has been documented for years but remains present in millions of projects because minimatch is a transitive dependency that nobody reviews.

You run npm ls minimatch in your project and see it’s used by glob, fast-glob, rimraf, del, and ten other dependencies you didn’t even know you had. Updating minimatch directly doesn’t help because each package brings its own version. And even if you updated it, performance remains the same.

The problem with backtracking regular expressions

Minimatch converts glob patterns to regular expressions. The pattern *.js becomes something like ^(?:(?!\.)(?=.)[^/]*?\.js)$. This expression works correctly, but when the pattern has alternatives like {a,b,c} or ranges like {1..100}, the resulting regular expression grows exponentially.

JavaScript’s regular expression engine uses backtracking to find matches. When one branch of the expression doesn’t match, the engine backtracks and tries another branch. With complex expressions, the number of combinations to try grows exponentially. A seemingly innocent pattern can generate millions of backtracking operations.

CVE-2022-3517 exploits exactly this. A specifically designed pattern can make the regex engine enter a backtracking loop that consumes CPU for seconds or minutes. On a web server, this means an attacker can take down your service by sending a request with a malicious pattern.

The solution: picomatch as internal engine

Picomatch is another glob matcher written by Jon Schlinkert. It uses a different approach to generate regular expressions that avoids catastrophic backtracking. Additionally, it has internal limits on brace expansion to prevent patterns like {1..1000000} from generating a million alternatives.

The problem with picomatch is that its API isn’t compatible with minimatch. If your code uses minimatch(path, pattern), you can’t simply switch to picomatch(pattern)(path). You’d have to rewrite all calls, update tests, and verify behavior is identical in all edge cases.

This is where minimatch-fast comes in. It’s a compatibility layer that exposes exactly the same API as minimatch but uses picomatch internally. You don’t have to change a single line of code. Install the package, update the import, and everything works the same but faster and without the vulnerability.

Installation and migration

Installation is standard:

npm install minimatch-fast

The simplest migration is changing imports:

// Before
import { minimatch } from 'minimatch';

// After
import { minimatch } from 'minimatch-fast';

If you don’t want to touch the code, you can use npm aliasing. This makes any package that imports minimatch receive minimatch-fast instead:

npm install minimatch@npm:minimatch-fast

With this option, all dependencies using minimatch automatically use minimatch-fast without knowing it. Useful when you have transitive dependencies you can’t control.

API identical to minimatch

The main function is minimatch(path, pattern, options). It receives a path, a glob pattern, and optional options. Returns true if the path matches the pattern.

import { minimatch } from 'minimatch-fast';

minimatch('src/index.ts', '**/*.ts');           // true
minimatch('src/index.ts', '**/*.js');           // false
minimatch('.gitignore', '*', { dot: true });    // true
minimatch('SRC/Index.ts', '**/*.ts', { nocase: true }); // true

To filter arrays of paths, use minimatch.match():

const files = ['app.js', 'app.ts', 'test.js', 'README.md'];
const jsFiles = minimatch.match(files, '*.js');
// ['app.js', 'test.js']

If you need a reusable filter for Array.filter():

const files = ['app.js', 'app.ts', 'test.js'];
const isTypeScript = minimatch.filter('*.ts');
const tsFiles = files.filter(isTypeScript);
// ['app.ts']

To get the compiled regular expression:

const regex = minimatch.makeRe('**/*.ts');
// Returns a RegExp object you can use directly
regex.test('src/index.ts'); // true

Brace expansion converts one pattern into multiple patterns:

minimatch.braceExpand('{src,lib}/*.js');
// ['src/*.js', 'lib/*.js']

minimatch.braceExpand('file{1..3}.txt');
// ['file1.txt', 'file2.txt', 'file3.txt']

To escape metacharacters when the pattern comes from user input:

const userInput = 'file[1].txt';
const escaped = minimatch.escape(userInput);
// 'file\\[1\\].txt'

minimatch('file[1].txt', escaped); // true (literal match)

The Minimatch class for reusable patterns

When you’re going to compare the same pattern against many paths, compiling the pattern once is more efficient:

import { Minimatch } from 'minimatch-fast';

const matcher = new Minimatch('**/*.{js,ts}');

const files = ['app.js', 'lib/utils.ts', 'README.md', 'src/index.tsx'];

const matches = files.filter(file => matcher.match(file));
// ['app.js', 'lib/utils.ts']

The class exposes useful properties:

const m = new Minimatch('!**/*.test.js');

m.pattern;   // '!**/*.test.js'
m.negate;    // true (pattern starts with !)
m.comment;   // false (not a comment)
m.regexp;    // compiled RegExp

The hasMagic() method indicates if the pattern has glob metacharacters:

new Minimatch('*.js').hasMagic();      // true
new Minimatch('index.js').hasMagic();  // false

Supported pattern syntax

Basic wildcards work as expected:

Braces expand alternatives:

minimatch('src/app.js', '{src,lib}/*.js');  // true
minimatch('lib/app.js', '{src,lib}/*.js');  // true
minimatch('dist/app.js', '{src,lib}/*.js'); // false

Numeric and alphabetic ranges also work:

minimatch('file3.txt', 'file{1..5}.txt');  // true
minimatch('file7.txt', 'file{1..5}.txt');  // false

minimatch('section-b.md', 'section-{a..d}.md'); // true

Extglob patterns add regular expression operators:

minimatch('foo.js', '*.+(js|ts)');   // true
minimatch('foo.jsx', '*.+(js|ts)');  // false

minimatch('test.spec.js', '!(*spec*).js'); // false (contains spec)
minimatch('app.js', '!(*spec*).js');       // true

POSIX classes provide portable character sets:

minimatch('file1.txt', 'file[[:digit:]].txt');  // true
minimatch('fileA.txt', 'file[[:alpha:]].txt');  // true
minimatch('FILE.txt', '[[:upper:]]*.txt');      // true

Negation inverts the match result:

minimatch('app.js', '!*.test.js');      // true (not a test)
minimatch('app.test.js', '!*.test.js'); // false (is a test)

Configuration options

Options control matching behavior:

dot: By default, * doesn’t match files starting with a dot. Enable this option to include them.

minimatch('.gitignore', '*');              // false
minimatch('.gitignore', '*', { dot: true }); // true

nocase: Case-insensitive matching.

minimatch('README.md', '*.MD');                // false
minimatch('README.md', '*.MD', { nocase: true }); // true

matchBase: Compare the pattern only against the filename, not the full path.

minimatch('src/lib/utils.js', '*.js');                  // false
minimatch('src/lib/utils.js', '*.js', { matchBase: true }); // true

noglobstar: Treat ** as * (doesn’t cross directories).

minimatch('a/b/c.js', '**/*.js');                    // true
minimatch('a/b/c.js', '**/*.js', { noglobstar: true }); // false

nobrace: Disable brace expansion.

minimatch('src/a.js', '{src,lib}/a.js');                 // true
minimatch('src/a.js', '{src,lib}/a.js', { nobrace: true }); // false

noext: Disable extglob patterns.

minimatch('foo.js', '*.+(js|ts)');                 // true
minimatch('foo.js', '*.+(js|ts)', { noext: true }); // false

nonegate: Disable negation with !.

minimatch('app.js', '!*.test.js');                   // true
minimatch('app.js', '!*.test.js', { nonegate: true }); // false

To create a matcher with default options:

const mm = minimatch.defaults({ nocase: true, dot: true });

mm('README.MD', '*.md');  // true
mm('.env', '*');          // true

Benchmarks: between 1.3x and 26x faster

Benchmarks were run on Node.js 22 comparing minimatch 10.0.1 against minimatch-fast 1.0.0. Results vary by pattern type:

Simple patterns (*.js, **/*.ts): 1.35x faster. The improvement is modest because these patterns are already efficient in minimatch.

Negation patterns (!*.test.js): 1.50x faster.

Brace patterns ({src,lib}/**/*.{js,ts}): 6.5x faster. This is where picomatch shines because it avoids the combinatorial explosion of alternatives.

Complex brace patterns ({a,b,c}/{d,e,f}/**/*.{js,ts,jsx,tsx}): 26.6x faster. Minimatch generates a giant regular expression while picomatch maintains linear performance.

Precompiled Minimatch class: 1.16x faster on repeated use of the same pattern.

In practical terms, if your build takes 10 seconds processing globs with complex patterns, it can drop to less than 1 second. For simple patterns the improvement is smaller but it’s never slower than minimatch.

Security: protection against ReDoS

CVE-2022-3517 enables denial of service attacks through regular expressions. An attacker sends a pattern designed to cause catastrophic backtracking and your server freezes.

minimatch-fast mitigates this in two ways:

Safe regex engine: Picomatch generates regular expressions that don’t suffer exponential backtracking. Patterns that freeze minimatch execute instantly in minimatch-fast.

Limits on brace expansion: Range expansion like {1..1000} has limits to prevent a pattern from generating millions of alternatives. Original minimatch freezes with {1..100000}. minimatch-fast processes it in milliseconds with a reasonable limit.

// This freezes minimatch for seconds or minutes
// minimatch('file.js', '{1..100000}.js');

// minimatch-fast handles it without issues
minimatch('file.js', '{1..100000}.js'); // false, processed in ms

If your application receives patterns from external users, minimatch-fast is the safe choice.

Compatibility verified with 355 tests

The package includes 355 tests that verify exact compatibility with minimatch:

All original minimatch tests pass. If your code works with minimatch, it works with minimatch-fast without changes.

Full TypeScript support

The package includes complete type definitions:

import {
  minimatch,
  Minimatch,
  type MinimatchOptions,
} from 'minimatch-fast';

const options: MinimatchOptions = {
  nocase: true,
  dot: true,
};

const matcher: Minimatch = new Minimatch('**/*.ts', options);
const result: boolean = matcher.match('src/index.ts');

Types are identical to @types/minimatch, so autocomplete and type checking work the same.

When to use minimatch-fast

Migration from minimatch: If you already use minimatch and want better performance or to eliminate the CVE-2022-3517 vulnerability, minimatch-fast is a one-line change.

New projects: If you’re going to use glob matching, start directly with minimatch-fast instead of minimatch.

Complex patterns: If you use patterns with many braces or ranges, the performance improvement is dramatic.

Applications receiving external patterns: If users can send glob patterns, minimatch-fast protects against ReDoS attacks.

Transitive dependencies: Use npm aliasing so all your dependencies use minimatch-fast automatically.

The code is on GitHub

The package is published on NPM as minimatch-fast. Source code is at github.com/686f6c61/minimatch-fast.

The license is MIT, same as original minimatch. You can use it in commercial projects without restrictions.

If you find incompatibilities with minimatch or have suggestions, open an issue on GitHub. The goal is to maintain 100% compatibility while improving performance and security.