Eglot ships with Emacs as a built-in, lightweight LSP client, and its default configuration is sufficient for most projects. However, working with large codebases can cause latency. When Eglot becomes sluggish, the problem may involve work performed by Eglot and Emacs as well as the language server itself. Event logging, filesystem watching, diagnostics, completion, and other editor activity can all contribute to latency.
This article covers practical changes for keeping Eglot responsive when working with large projects.
Auto-shutting down idle Eglot servers
This reduces resource usage when switching between multiple projects. The eglot-autoshutdown variable shuts down the LSP server after the last managed buffer has been killed.
(setq eglot-autoshutdown t)
This is useful when many separate language servers would otherwise remain running after their buffers have been closed.
(If you find yourself routinely leaving unused buffers open, the buffer-terminator package can automate this cleanup. It quietly monitors your buffer activity and terminates idle file buffers, ensuring eglot-autoshutdown can trigger reliably without requiring you to manually execute kill-buffer.)
Preventing Eglot from blocking the Emacs UI on connection
Opening a project can cause Emacs to block while waiting for the LSP server to initialize. Setting eglot-sync-connect to nil prevents Eglot from blocking on the initial connection, allowing the connection to continue in the background:
(setq eglot-sync-connect nil)
This prevents the connection attempt from unnecessarily delaying interactive editing.
Disabling LSP event logging
Eglot maintains an events buffer containing JSON-RPC activity. Large event logs consume memory and add string allocation costs, especially when retaining complete JSON payloads is not needed during normal editing.
Configuring eglot-events-buffer-config (or eglot-events-buffer-size on Emacs 29 and earlier) reduces the volume of retained data:
;; Disable event logging completely (Emacs >= 30)
(setq eglot-events-buffer-config '(:size 0 :format short))
;; For Emacs <= 29
;; (setq eglot-events-buffer-size 0)
Setting :size 0 disables the events buffer.
Reducing file watchers
Allowing a language server to monitor large portions of a repository can consume OS file descriptors, increase memory usage, and add startup delay. To reduce these resource costs, limit the number of file that Eglot watches using:
(setq eglot-max-file-watches 5000)
In addition, where supported by the language server, configure it to ignore large generated directories such as node_modules, .git, or build output folders. Because these exclusion rules are specific to the language server, they must be passed through the eglot-workspace-configuration variable. (The standard method is to define these rules per-project using a .dir-locals.el file at the root of your repository.)
Disabling progress reporting
Whenever a server reports progress (e.g., during indexing or builds), this can trigger mode-line redisplays. The following suppress mode-line progress reports:
;; Suppress mode-line progress animations
(setq eglot-report-progress nil)
Disabling automatic code action probing
By default, Eglot can asynchronously query the language server for available code actions at point when the cursor is idle. Disabling these automatic indications reduces continuous process communication and background activity during editing:
;; Disable automatic code action indicators to reduce background polling
(setq eglot-code-action-indications nil)
Note: Disabling this feature only removes the automatic UI indicators. You retain the ability to manually request and execute code actions at any time by invoking M-x eglot-code-actions.
Disabling unneeded LSP server capabilities
Note: You do not need to disable all of these capabilities, as some of them are very useful. Only disable the ones you do not use.
LSP servers can provide optional capabilities. Disabling features that are not needed can reduce processing and visual clutter, although the actual performance benefit depends on the language server and project.
Eglot handles on-type formatting by evaluating every single keystroke to check if the connected language server supports the feature and if the typed character is a registered trigger character. If a match occurs, Eglot issues an RPC request and applies the asynchronous edits, which can conflict with external formatters and introduce typing latency. To prevent these background requests and bypass the per-keystroke evaluation entirely, use:
(with-eval-after-load 'eglot
(add-to-list 'eglot-ignored-server-capabilities :documentOnTypeFormattingProvider))
Inlay hints (eglot-inlay-hints-mode) display automatically determined types and parameter names as annotations in the buffer. Disabling them removes the associated visual annotations and any work required to maintain them:
(with-eval-after-load 'eglot
(add-to-list 'eglot-ignored-server-capabilities :inlayHintProvider))
When the cursor rests on a symbol, the LSP server can highlight other occurrences of that symbol (eglot-highlight-eldoc-function). Disabling it will stop Eglot from highlighting other occurrences of the symbol under the cursor:
(with-eval-after-load 'eglot
(add-to-list 'eglot-ignored-server-capabilities :documentHighlightProvider))
For major modes with Tree-sitter support, Tree-sitter already provides syntax-aware fontification locally and can be sufficient for many users. When the language server supports semantic tokens, Eglot can request semantic-token information over LSP and use it for additional syntax highlighting. Processing these responses requires JSON-RPC communication, decoding the token data, and applying the corresponding fontification. On large or frequently changing buffers, this can add CPU overhead. Ignoring the server's semantic-token capability prevents Eglot from using this feature:
(with-eval-after-load 'eglot
(add-to-list 'eglot-ignored-server-capabilities :semanticTokensProvider))
Note: The tradeoff is that some semantic highlighting supplied by the language server may be lost. Tree-sitter fontification and LSP semantic tokens are not identical, so disabling semantic tokens can result in less precise or less detailed highlighting for some languages.
Garbage collection, native compilation, and read process output max
- The Emacs garbage collector can cause pauses during heavy workloads. LSP activity can increase allocation through diagnostics, completion, JSON processing, and other editor integrations. GC tuning can therefore help in some workloads, but the effect is workload-dependent. Increasing
gc-cons-thresholdpermanently can reduce garbage collection frequency:(setq gc-cons-threshold (* 100 1024 1024))
(Alternatively, several users rely on thegcmhpackage, which raises the GC threshold during active editing and forces a collection when Emacs becomes idle.) - Enable Native Compilation: Ensure that Emacs is built with native compilation support enabled. Native compilation can improve the execution speed of Emacs Lisp code, including code used by Eglot and other packages. (Recommendation: Use the compile-angel package to ensure that all packages are natively compiled.)
- read-process-output-max:
read-process-output-maxcontrols the maximum amount of data Emacs reads from a subprocess in a single operation. Since Eglot communicates with language servers through subprocesses, increasing this value can improve performance when a server sends large bursts of JSON-RPC data, such as during initialization, workspace indexing, or large file updates, by reducing the number of read operations required to consume the output. Raising it can help workloads that regularly receive large bursts of process output:(setq read-process-output-max (* 1024 1024))
(This raises the limit to 1 MiB. It does not reserve 1 MiB of memory for each process or increase the amount of data a server can send; it only allows Emacs to consume more output per read operation. On GNU/Linux systems, the value should not exceed/proc/sys/fs/pipe-max-size)
Freeing up the main Emacs thread with tree-sitter
Emacs executes Lisp, handles asynchronous process output, and updates the UI on a single main thread. In traditional major modes, typing triggers complex regular expression evaluations for syntax highlighting (font-locking). This CPU-bound work competes directly with Eglot, which relies on the exact same thread to deserialize incoming JSON-RPC payloads and render diagnostics. If the main thread is busy computing regexes, Eglot is forced to wait in the queue, resulting in input lag even if the external language server responds instantly.
Major modes built on Tree-sitter (the *-ts-mode variants, such as c-ts-mode, python-ts-mode, and rust-ts-mode) use the Tree-Sitter C library for incremental syntax parsing. If a stable *-ts-mode exists for your programming language, enabling it can improve Eglot's responsiveness.
Here is a code snippet from my configuration that I have been using for the last few months to automatically calculate this value, in case anyone is interested:
;; Increase single chunk bytes to read from subprocess (setq read-process-output-max (or (when (eq system-type 'gnu/linux) (condition-case nil ;; On GNU/Linux systems, the value should not exceed ;; /proc/sys/fs/pipe-max-size (with-temp-buffer (insert-file-contents "/proc/sys/fs/pipe-max-size") (string-to-number (buffer-string))) (error nil)) (* 1024 1024))))One other thing that I did to speed up Python in Eglot: I replaced multiple distinct tools, such as autopep8, mccabe, pyflakes, and isort, with Ruff for all formatting and linting.
Here is a comment related to this article that I posted on Reddit, which may interest readers:
Eglot's on-type formatting is currently synchronous. When a trigger character is typed, eglot--post-self-insert-hook calls eglot-format:
(defun eglot--post-self-insert-hook () "Set `eglot--last-inserted-char', maybe call on-type-formatting." (setq eglot--last-inserted-char last-command-event) (let ((ot-provider (eglot-server-capable :documentOnTypeFormattingProvider))) (when (and ot-provider (ignore-errors ; github#906, some LS's send empty strings (or (eq eglot--last-inserted-char (seq-first (plist-get ot-provider :firstTriggerCharacter))) (cl-find eglot--last-inserted-char (plist-get ot-provider :moreTriggerCharacter) :key #'seq-first)))) (eglot-format (point) nil eglot--last-inserted-char)))) ;; This hook runs when a character is inserted. (add-hook 'post-self-insert-hook #'eglot--post-self-insert-hook nil t)The eglot-format function then calls eglot--request, which calls jsonrpc-request. This is a blocking call. Emacs waits for the language server to compute and return the text edits before control is returned to the user.
Regarding your second question: Eglot enables this feature by default based on the language server's advertised capabilities. If a server includes :documentOnTypeFormattingProvider in its capability response, Eglot assumes the server intends for it to be used and hooks into post-self-insert-hook.
Note: as stated in the article, users who do not need this intrusive feature can make Eglot ignore the capability by adding it to eglot-ignored-server-capabilities: