Open-source project
mantoni/eslint_d.js avatar
mantoni/eslint_d.js

eslint_d, a persistent eslint server so the editor stops paying startup cost

🪄 Speed up eslint to accelerate your development workflow

1,224 stars73 forksJavaScriptMIT

At a glance

What is it?
eslint_d runs eslint in a background process and talks to it over a Unix domain socket, so an editor that lints on save pays the module loading cost once instead of on every keystroke. The measured comparison in the README is about 300ms for eslint against about 100ms for eslint_d on a MacBook Air M4.
Who is it for?
eslint_d suits a Vim, Neovim, Emacs or Sublime user who lints on save and finds the node startup cost annoying, since the daemon keeps the eslint process warm and the change to your configuration is a single executable path. It is the wrong tool for VSCode, Atom or WebStorm, because the README says plainly that those editors already cache eslint instances and you will not gain any performance.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 84 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The cost being avoided is module loading, not linting

eslint_d runs eslint in a background process to improve linting time while editing, and the reason is stated plainly in the How does this work section. It starts a background server that runs eslint in a separate process, communicates with the server over a Unix domain socket, and when you run eslint_d it forwards all arguments to the server and prints the result. This is faster because node.js doesn't have to load all the required modules every time.

That is the whole mechanism, and it explains the numbers the README gives at the top. On a MacBook Air M4 with node.js v24.14.0 and eslint v10.0.3:

bash
❯ eslint file.js # ~300ms
❯ eslint_d file.js # ~100ms

Roughly a third of the time, and the saving is the module load. The actual lint work is unchanged, which is why the win is biggest for a small file and smaller for a large one, and why the tool matters most in an editor that lints on every save.

The features list is short. It supports all eslint versions from v4 to v10 and all LTS versions of node.js. It automatically starts, stops and restarts the background server. It can bind to the parent process, to the editor process, or exit after an idle time. And it falls back to the bundled eslint if the local eslint is missing.

It is a drop-in replacement for eslint, and the command form is the same shape:

code
eslint_d [options] file.js [file.js] [dir]

All arguments are passed through to eslint, except for a short list of daemon commands of its own: start, stop, restart, status, --help, --version, and --fix-to-stdout. status is the one you will use most, since it shows the daemon status, the process id and the resolved eslint version, which answers the question of which eslint is actually running.

The package is eslint_d on npm, the licence is MIT, the package version in the repository is 15.0.3, the engines field requires node 22 or newer, and the last push was on 2026-07-31. The author is Maximilian Antoni.

Which editors benefit, and which already solved it

The editor list is the most practically useful part of the README, because it includes an explicit warning about the editors where this does nothing.

For Vim with ALE, you set the parent process id and point ALE at the daemon:

vim
let $ESLINT_D_PPID = getpid()
let g:ale_javascript_eslint_executable = 'eslint_d'
let g:ale_javascript_eslint_use_global = 1

For Vim with syntastic, the same idea with a different variable name:

vim
let $ESLINT_D_PPID = getpid()
let g:syntastic_javascript_checkers = ['eslint']
let g:syntastic_javascript_eslint_exec = 'eslint_d'

For Neovim with nvim-lint, the linters are configured per file type:

lua
vim.env.ESLINT_D_PPID = vim.fn.getpid()
require('lint').linters_by_ft = {
  javascript = {'eslint_d'},
  typescript = {'eslint_d'},
}

For Emacs, flycheck's javascript-eslint checker is pointed at the executable:

elisp
(setq flycheck-javascript-eslint-executable "eslint_d")

For Sublime, the official SublimeLinter-eslint plugin automatically prefers eslint_d if it finds one, so there is no configuration to do.

And then the negative case, which deserves as much attention as the positive ones. For Atom, VSCode and WebStorm, the README says you will not gain any performance from this module as these editors already cache eslint instances for you. That is a genuinely useful piece of information, because the natural assumption is that any editor integration is worth trying, and here three of the most popular ones have already solved the problem in a different way.

The README also asks that if you are using eslint_d in any other editor, you let the author know, which is a reasonable request for a project whose integration surface is a handful of lines of configuration per editor.

Four environment variables that decide the daemon's behaviour

The daemon's lifecycle is the part that can go wrong, and it is controlled by four environment variables.

ESLINT_D_PPID is the parent process id to monitor. If the parent process dies, the daemon exits as well. The value 0 disables monitoring, which is the default, and the value auto monitors the parent process that started eslint_d. The README is careful about one thing here: auto uses the parent process that started eslint_d, which may not be the editor process. That is a real trap, because the process that launches eslint_d on a buffer save is often a short-lived shell, so the daemon can die with it.

ESLINT_D_IDLE is the number of minutes of inactivity before the daemon exits. It defaults to 0 if ESLINT_D_PPID is set, and to 15 otherwise. So a daemon that is tied to a parent process has no idle timeout, and a free-standing one gives up after a quarter of an hour.

ESLINT_D_MISS decides what happens when the local eslint is missing. The value fallback uses the bundled eslint, which is the default. The value fail logs an error and exits with code 1. The value ignore silently exits with code 0. That last one deserves a pause, because a lint command that exits 0 without linting is the worst failure mode for an editor integration, since the editor will report no errors. If your project depends on a specific eslint version and its plugins, ignore is the wrong setting.

ESLINT_D_ROOT provides a specific directory to search for node_modules/eslint, and the README says it is useful in monorepos where node_modules might be in a location other than the project root.

The same behaviour is documented in the How does this work section in the terms of what the daemon does with files. A .eslint_d file is stored in the resolved eslint installation directory, and it stores a security token, the server port and process id, and the hash of the monitored files. If the file is removed, the server exits. That file is the mechanism, and it is also why the daemon can detect a changed project.

The server is also automatically restarted if one of package.json, package-lock.json, npm-shrinkwrap.json, yarn.lock or pnpm-lock.yaml changed. Those are the files whose change would invalidate the loaded configuration or the installed plugin set, which is exactly the right trigger list.

Fixing on save, and the one option eslint does not have

eslint_d has an additional option that eslint does not have, --fix-to-stdout, which prints the fixed file to stdout. The README explains the point: it lets editors add before save hooks to automatically fix a file prior to saving. It must be used with --stdin.

The Vim mappings show the pattern. For the entire buffer:

vim
" Autofix entire buffer with eslint_d:
nnoremap <leader>f mF:%!eslint_d --stdin --fix-to-stdout --stdin-filename %<CR>`F
" Autofix visual selection with eslint_d:
vnoremap <leader>f :!eslint_d --stdin --fix-to-stdout<CR>gv

The first one pipes the buffer through eslint_d, which reads from stdin, fixes, and writes the fixed content to stdout, and then the mapping replaces the buffer with the output. The --stdin-filename flag is there because eslint needs a filename to resolve the config and the ignore rules for that file, and without it the rules applied would be the wrong ones. The second mapping does the same for a visual selection.

For Emacs, the README points at a separate project, eslintd-fix, rather than providing a snippet.

There is also a debug mode, added in v14.2.0, enabled with the --debug flag. The DEBUG environment variable limits the debug output to eslint_d:

bash
❯ DEBUG="eslint_d:*"

And for server side output you restart with the flag in a separate terminal, which keeps the process attached to the terminal and prints the debug output:

bash
❯ eslint_d restart --debug # eslint and eslint_d logs
❯ DEBUG="eslint_d:*" eslint_d restart --debug # eslint_d logs only

The two variants are the useful distinction here, since a problem in the daemon and a problem in the eslint process inside it need different logs, and the second command isolates them.

A compatibility table that explains the version scheme

The compatibility section is a list of version lines with the eslint versions and node versions each supports, and it reads backwards through the history in a way that explains how the project has been maintained.

The current line, 15.0.0, supports eslint 4 to 10 and node 22 to 24, and ships with eslint 10. Before that, 14.0.0 supported eslint 4 to 9 and node 18 to 22 and shipped with eslint 9. 13.0.0 supported eslint 4 to 8 and node 12 to 20, and 12.0.0 supported eslint 4 to 8 and node 12 to 16, shipping with eslint 8. 11.0.0 was still on eslint 7, and 10.0.0 and 9.0.0 are where the API change shows up, with 10.0.0 using the new ESLint API if available and 9.0.0 using the CLIEngine API. Older lines go back to 6.0.0 with eslint 5.0 and node 6.

Two things are worth drawing out of that table.

First, the eslint range is consistently wide, four to ten, which is unusual for a wrapper and tells you the tool is written defensively against a moving API. Second, the node requirement has moved forward steadily and aggressively. The current line needs node 22 to 24 and package.json declares engines node >=22, which means the current release of eslint_d will not install on an older node, even though the tool is capable of driving eslint 4.

The project also ships a bundled eslint, which is what makes the fallback work, and the eslint dependency in package.json is 10.0.3. The bundled copy is why a user with no local eslint still gets linting, and it is also why the version mismatch can confuse people, which is what status is for.

The repository itself is a plain npm package with an eslint.config.js, a lib/ directory, a bin/ directory, test/, scripts/, a .husky/ directory and a .lintstagedrc.js, and the build tooling is mocha for the unit tests plus a separate integration test script with --bail and a five second timeout, and a coverage script using the v8 coverage provider. The version script runs changes --commits --footer, which is how the changelog entries are generated from commit messages.

Editorial conclusion

eslint_d suits a Vim, Neovim, Emacs or Sublime user who lints on save and finds the node startup cost annoying, since the daemon keeps the eslint process warm and the change to your configuration is a single executable path. It is the wrong tool for VSCode, Atom or WebStorm, because the README says plainly that those editors already cache eslint instances and you will not gain any performance. Before adopting it, read the ESLINT_D_PPID guidance carefully, since binding the daemon lifetime to the wrong process leaves a server running, and decide whether the bundled eslint fallback is acceptable, because ESLINT_D_MISS controls whether a missing local eslint is a fallback, a failure or a silent no-op.

Frequently asked questions

How do I install eslint_d and use it with Vim?

Install it with npm i -g eslint_d, then in your Vim configuration set $ESLINT_D_PPID to getpid() and point the linter at the daemon. With ALE that is g:ale_javascript_eslint_executable set to eslint_d, and with syntastic it is g:syntastic_javascript_eslint_exec.

Which editors does eslint_d not help with?

Atom, VSCode and WebStorm. The README states you will not gain any performance from this module as these editors already cache eslint instances for you.

What happens if my project does not have eslint installed locally?

By default eslint_d falls back to the bundled eslint package. You can change this with the ESLINT_D_MISS environment variable, where fail logs an error and exits with code 1 and ignore silently exits with code 0.

How do I stop the daemon from lingering after I close my editor?

Set ESLINT_D_PPID to the parent process id to monitor, or to auto to monitor the parent process that started eslint_d, and the daemon exits when that process dies. ESLINT_D_IDLE sets the inactivity timeout in minutes, which defaults to 0 when the parent id is set and 15 otherwise.

How can I automatically fix a file before saving it?

Use the --fix-to-stdout option, which prints the fixed file to stdout and does not exist in eslint itself. It must be combined with --stdin, and the README shows a Vim mapping that pipes the buffer through it.

Official sources

  1. Issues
  2. License: MIT
  3. mantoni/eslint_d.js on GitHub
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/mantoni-eslint-d-js.svg)](https://hysenlabs.com/projects/mantoni-eslint-d-js)

Was this useful?