Haskell Language Server Troubleshooting for Real Projects
You open a module, the editor shows a spinner for forty seconds, and then every import is underlined in red even though cabal build finished cleanly a minute ago.

You open a module, the editor shows a spinner for forty seconds, and then every import is underlined in red even though cabal build finished cleanly a minute ago. That is the most common Haskell Language Server complaint I hear from people who join a team codebase, and in my experience nine times out of ten the server itself is fine. Something around it is mismatched.
This guide is the checklist I run before I blame HLS. It covers GHC version pairing, the cradle that tells the server how to load your project, memory on large repositories, and the plugins that quietly eat CPU.
Why Haskell Language Server Breaks More Often Than Other Language Servers
Most language servers parse source files and stop there. HLS does something heavier: it embeds the GHC API and typechecks your code with the exact compiler your project uses. That gives you accurate types on hover and real error messages, but it also means the server binary is tied to one specific GHC version. A server built for GHC 9.6.6 cannot load a project compiled with 9.8.2, and it will not always say so clearly.
The second source of trouble is that HLS has to reproduce your build. It asks Cabal or Stack for the flags, package databases and source directories of every component. If that question returns the wrong answer, you get phantom errors that disappear the moment you run a normal build.
Match the Server to Your Compiler First
Start here. The wrapper binary, haskell-language-server-wrapper, reads your project, works out which GHC it needs, and launches the matching server binary. If that binary is missing, the wrapper may fall back to whatever it finds on your PATH.
- Run ghc with the numeric version flag inside the project folder and note the exact version.
- Open ghcup tui and check that a server build supporting that GHC is installed. Supported versions are marked in the list.
- If your GHC has no prebuilt server, either move the project to a supported minor release or let ghcup compile HLS for it, which takes 20 to 40 minutes on a laptop.
- Restart the editor fully. Reloading the window is not always enough, because the old server process can survive.
I covered the toolchain side in more depth in our guide to setting up a Haskell environment, including why ghcup is the least painful way to keep GHC, Cabal and HLS in step.
Read the Server Log Before Changing Anything
Every editor exposes the HLS output somewhere. In VS Code it is the Output panel with the Haskell channel selected. In Neovim it depends on your LSP client, but the log file path is printed at startup. Look for three lines in particular:
- The GHC version the server reports it was built with.
- The cradle type it chose, which tells you whether it is using Cabal, Stack or a direct GHC invocation.
- The first error after the line that mentions loading the cradle.
That first error is the real one. A missing package database, a failed preprocessor such as hpack or alex, or a Template Haskell splice that needs a C library will all show up there before the red underlines appear in your files.
Fix the Cradle With a Small hie.yaml
HLS uses a library called implicit hie to guess how your project is built. It guesses well for a single package. It guesses badly for repositories with several packages, test suites that share modules with the library, or a mix of Cabal and Stack files left over from a migration.
When the guess is wrong, write a hie.yaml at the repository root. For a Cabal project with a library, an executable and a test suite, it needs only a few lines: a top level cradle key, a cabal key, and a list of components, each pairing a path such as src or test with a component name such as lib:myapp or test:myapp-tests. Being explicit stops HLS from loading a test module as if it belonged to the library, which is what produces those confusing errors about hidden packages like hspec.
When a monorepo needs more than one cradle
If one folder holds a Stack project and another holds plain Cabal packages, use a multi cradle and map each directory to its own tool. It looks clumsy, but it beats letting the server try every option on every file open.
The Advice to Disable Plugins Is Usually Wrong
Forum threads about slow HLS almost always end with someone saying to turn off every plugin. I disagree. Most plugins cost very little, and disabling all of them throws away the reason you installed the server: inline type signatures, import suggestions and quick fixes for missing pattern cases.
A small number of plugins do carry a real cost on large modules. Profile before you cut anything.
| Plugin | Typical cost | What I do |
|---|---|---|
| hlint | Moderate on files over 1,000 lines | Keep it, run on save only |
| Formatter (ormolu or fourmolu) | Low, only when invoked | Keep it |
| Eval | High if comments contain many examples | Disable in big repositories |
| Rename | High across many modules | Use it, but expect a pause |
The plugin that most often surprises people is Eval. It re-runs example expressions in comments, and one heavily documented module can keep a core busy for minutes.
Keep Memory Under Control on Large Codebases
On a project with around 400 modules I regularly see HLS reach 6 to 8 GB of resident memory after a long session. That is not a leak in the usual sense. The server keeps typechecked modules and their interface files in memory so that hover and go to definition stay instant.
Three changes help without hurting responsiveness much:
- Set the server to check only open files rather than the whole project. You lose project wide diagnostics, but your build or CI still catches those.
- Pass runtime options to cap the heap. A limit around 4 GB forces more frequent collection and keeps the machine usable. If you want to understand what the collector is doing, the RTS statistics report applies to HLS just as it does to your own programs.
- Restart the server once or twice a day. Every editor integration has a command for it.
FAQ
Should I use HLS or ghcid for day to day work?
Both, if you can. HLS gives you hover, completion and quick fixes inside the editor. Ghcid runs a GHCi session in a terminal and reports errors faster on very large projects. Many developers keep ghcid open as the source of truth and treat HLS as a navigation tool, a habit that pairs well with a GHCi centred workflow.
Why does HLS work for the library but not the test suite?
Almost always a cradle issue. The test component has dependencies such as hspec or tasty that the library does not, and if HLS loads test files with the library component it cannot find them. An explicit hie.yaml entry for the test directory fixes it.
Is it safe to delete the HLS cache?
Yes. The cache lives under your user cache directory in a folder named ghcide. Deleting it forces a full reindex on the next start, which can take several minutes on a big project, but it clears stale interface files that cause errors nothing else explains.
A Short Routine That Saves Hours
When Haskell Language Server misbehaves, work through the same order every time: confirm the GHC version, read the first error in the log, write an explicit hie.yaml, then look at memory and individual plugins. Most problems end at step two or three. Write your working hie.yaml into the repository so the next person who joins never sees that forty second spinner, and browse the rest of our developer tools articles when you are ready to tune the rest of your setup.
More in Developer Tools
Developer Tools
Choosing a Haskell Code Formatter for Your Team
Your team keeps arguing about where the arrows go in a do block, and every pull request carries twenty lines of whitespace...
Developer Tools
How to Speed Up Cabal Builds on Large Haskell Projects
A clean build of a mid-sized Haskell service can take twelve minutes, and an incremental one after touching a single core...
Developer Tools
Using GHCi as Your Daily Haskell Workflow
Many Haskell beginners treat GHCi as a calculator they poke at occasionally, then do their real work by compiling full programs...