Why is RuboCop slow when an editor or git hook runs it per file, and what do its --server mode and --lsp language server change?
answer
- boot cost of requiring RuboCop
- --server keeps a forked daemon warm
- restarts when Gemfile.lock or config changes
- not on Windows or JRuby
- --lsp speaks the protocol on stdio
basics
~20 sEach rubocop invocation spends much of its time loading RuboCop and its plugins. --server keeps a background process with everything loaded and forwards later commands to it; --lsp runs a long-lived language server that editors talk to directly.
solid answer
~40 sMost of a short `rubocop` run is **boot time**: requiring RuboCop, its cops and plugins. `rubocop --server` starts a background server for the project directory with that runtime already loaded, and later `rubocop` commands are forwarded to it. `--start-server`, `--stop-server`, `--restart-server` and `--server-status` manage it, and putting `--server` in a `.rubocop` options file or `RUBOCOP_OPTS` makes it the default. The server restarts itself when `Gemfile.lock` or local config files change; it needs `fork`, so it is unavailable on Windows and JRuby. `rubocop --lsp` is different: it runs a **language server** over standard input and output, so an editor gets diagnostics as you type and safe autocorrect on format, with `-A` available through an initialization option. Use `--server` for terminals and hooks, `--lsp` for editors.
code
bash · 5 linesbundle exec rubocop --start-server
bundle exec rubocop app/models/order.rb # forwarded to the warm server
bundle exec rubocop --server-status
echo "--server" >> .rubocop # make server mode the default
bundle exec rubocop --stop-servergo deeper
Recall that --server keeps RuboCop loaded between runs and that editors use --lsp through a plugin.
Explain boot cost, the server's commands and restart triggers, and how LSP formatting maps to -a with -A as an opt-in.
Diagnose stale results from a server, choose server or LSP per workflow, and know why contextual cops do not autocorrect in the editor.
Standardise developer tooling so editors, hooks and CI run the same bundled RuboCop and configuration, with fast paths that cannot drift.
## Where the time goes A `rubocop` run on one small file is dominated by **start-up**: Ruby has to load RuboCop, its hundreds of cop classes and every plugin before inspecting a single line. That is fine for one CI run and painful for an editor that lints on every save, or a pre-commit hook that runs it several times. RuboCop offers two long-lived processes to pay that cost once. ## Server mode (`--server`) Server mode, added in RuboCop 1.31, keeps a background process per project directory with the RuboCop runtime already required: - **`rubocop --server`** starts the server if none is running, then runs the inspection through it. Later plain `rubocop` commands in that project are also sent to the running server. - **`rubocop --start-server`** only starts it and prints its address, for example `RuboCop server starting on 127.0.0.1:55772.` - **`--stop-server`**, **`--restart-server`**, **`--server-status`** manage it; **`--no-detach`** keeps it in the foreground, which suits containers; **`--no-server`** stops it and runs without. - **Default on:** if a `.rubocop` options file in the project root or the `RUBOCOP_OPTS` environment variable contains `--server`, every `rubocop` command uses server mode. - **Host and port:** `RUBOCOP_SERVER_HOST` and `RUBOCOP_SERVER_PORT` override the defaults. ### Staleness The server's cache key includes the contents of `Gemfile.lock` (or `gems.locked`) and the local configuration, so after `bundle update` or an edit to `.rubocop.yml`, `.rubocop_todo.yml` or a local file named by `inherit_from` or `require`, the next command prints `RuboCop version incompatibility found, RuboCop server restarting...`. Changes to remote `inherit_from` URLs or to files found on `$LOAD_PATH` are **not** detected; `--restart-server` handles those. ### Limits The server forks, so it is available only on CRuby and not on Windows or JRuby. It helps repeated short runs; a single full CI run gains little. ## Language server (`--lsp`) `rubocop --lsp`, added in 1.53, is a **Language Server Protocol** server. An editor plugin launches it (usually as `bundle exec rubocop --lsp`) and talks to it over standard input and output: 1. The editor sends the buffer as you type; RuboCop replies with diagnostics. 2. "Format document" applies **safe** autocorrection, equivalent to `-a`. 3. An `initializationOptions` value `safeAutocorrect: false` switches formatting to `-A`, and the `rubocop.formatAutocorrectsAll` command runs `-A` on demand; `lintMode` and `layoutMode` restrict it to `Lint` or `Layout` cops. 4. Cops configured with `AutoCorrect: contextual`, such as `Lint/UselessAssignment` and `Lint/UnusedMethodArgument`, report but do not autocorrect while editing, so formatting on save does not delete a variable you are still writing. `--lsp` always runs in the current process, even when a server is running, because it serves its protocol on stdio. `--editor-mode` gives the same contextual behaviour to command-line tools that act on behalf of an editor. ## Setting it up in practice - **Editor:** install the editor's RuboCop extension or LSP client configuration and point it at `bundle exec rubocop --lsp`, so the editor uses the project's bundled RuboCop version and plugins rather than a global one. - **Terminal and hooks:** add `--server` to a `.rubocop` file at the project root; the first run starts the server and later runs reuse it. - **Containers:** start it with `--start-server --no-detach` when a process supervisor should own it. - **After upgrades:** a changed `Gemfile.lock` restarts the server automatically; after changing a remote shared config, run `--restart-server`. If results ever look stale, `rubocop --server-status` shows whether a server is running, and `--no-server` runs one command without it to compare. ## Choosing | Need | Use | |---|---| | Real-time diagnostics and format-on-save in an editor | `--lsp` through the editor's RuboCop plugin | | Fast repeated runs from a terminal or git hook | `--server`, or `--server` in `.rubocop` | | One CI run | neither; a plain run, with the result cache | Both read the project's `.rubocop.yml`, so with the same bundled RuboCop version they apply the same rules as a plain command-line run.
- You changed an inherit_from URL's remote file, but rubocop --server still reports the old rules. Why?The server restarts automatically only when it detects changes to `Gemfile.lock` and local configuration files, including local paths named by `inherit_from` or `require`. It does not detect changes to remote files or to files found on `$LOAD_PATH`. Run `rubocop --restart-server` to reload.
- Why does formatting a file through rubocop --lsp not delete an unused local variable while rubocop -a on the command line does?`Lint/UselessAssignment` ships with `AutoCorrect: contextual`, which allows its correction from the `rubocop` command but not while editing through the language server or `--editor-mode`. The editor still shows the offense; it just will not remove a variable you may be about to use.
saying these in an interview costs you the question
- rubocop --server makes a single full CI run much faster.
- The RuboCop server never notices a .rubocop.yml change until it is restarted by hand.
- rubocop --lsp is meant to be run by hand in a terminal to lint files.
- Server mode works the same on Windows and JRuby as on CRuby.
- Formatting through --lsp applies unsafe corrections by default.