skip to content

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?

level: middleimportance: nice to knowfreq 25%

answer

  1. boot cost of requiring RuboCop
  2. --server keeps a forked daemon warm
  3. restarts when Gemfile.lock or config changes
  4. not on Windows or JRuby
  5. --lsp speaks the protocol on stdio

basics

~20 s

Each 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 s

Most 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 lines
bash
bundle 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-server

go deeper

for a junior

Recall that --server keeps RuboCop loaded between runs and that editors use --lsp through a plugin.

for a middle

Explain boot cost, the server's commands and restart triggers, and how LSP formatting maps to -a with -A as an opt-in.

for a senior

Diagnose stale results from a server, choose server or LSP per workflow, and know why contextual cops do not autocorrect in the editor.

for a principal

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.