emacs-async moves slow Emacs Lisp work into a child process
Simple library for asynchronous processing in Emacs
At a glance
- What is it?
- emacs-async is a small Emacs Lisp library that runs your Lisp code in a separate Emacs process started with -Q, so a slow operation no longer freezes the editor. It ships four ready made applications on top of the core, and its documentation is unusually honest about the two things that catch people out: leaking process buffers and the auth-source behaviour of the -Q switch.
- Who is it for?
- Adopt emacs-async if your Emacs sessions stall on TRAMP file operations, package compilation or mail sending, and you want those jobs moved into a child process without rewriting them. Do not adopt it as a general concurrency framework, since it is specifically about running Emacs Lisp in a child Emacs started with -Q, and remember that the child has none of your configuration unless you inject it.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 86 days ago.
- What is it written in?
- Mainly Emacs Lisp, 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
Emacs is single threaded, and that eventually hurts
The README opens with a first person explanation of the problem, which is the most honest framing the project could use. The author has been using Emacs long enough to know that its single-threaded nature is both a strength and a weakness. It keeps things simple, until you need to copy a hundred files over TRAMP, byte-compile a stack of packages, or send an email through a slow SMTP server. At that point your entire editor locks up and you are stuck watching the modeline blink. That last image is the accurate one. Emacs does not become slow in these cases, it stops, because there is one thread and the operation is synchronous. A package installation that takes a minute is a minute in which you cannot type. async.el fixes this by letting you run Emacs Lisp code in a child Emacs process. The child starts with -Q, so no init files and no fuss, executes whatever you give it, and passes the result back. Your main Emacs never blocks. The -Q choice is the interesting design decision and it has consequences on both sides. On the plus side, the child is deterministic: it does not load your configuration, so it cannot be affected by a broken init file or by a package that changes the semantics of the function you are calling. On the minus side, the child has none of your settings, which is why the library has to provide a way to pass variables across, and why the authentication section of the README exists at all. The audience is Emacs users whose workflows involve slow, external operations: file transfers over TRAMP, package builds, mail sending.
async-start and the callback that receives the answer
The core entry point takes a function and a callback.
(async-start
(lambda ()
(sleep-for 3)
42)
(lambda (result)
(message "Got %s" result)))The first argument is the body to run in the child, the second is the function called in the parent when the child returns. The child evaluates the lambda, and its value comes back as the argument to the callback. The result here is 42, printed by the message call. If you would rather wait for the result yourself, the README says to skip the callback and use async-get.
(let ((future (async-start
(lambda ()
(sleep-for 3)
42))))
;; do other work...
(message "Result: %s" (async-get future)))This is the shape you want when the caller has to do something with the value synchronously. The future is a handle you can pass around and block on, and the comment in the example makes the intent clear: you do other work first, then collect. The distinction between the two forms is the difference between fire and forget and request and reply, and having both in one library is what makes it usable from synchronous code as well as from event handlers.
The 'ignore callback and the process buffers you leak
There is one piece of advice in the README that deserves its own section, because the failure it prevents is invisible until your machine fills up with buffers. If you do not care about the return value, pass 'ignore as the callback. But the README is explicit: don't just leave it nil, because you will leak process buffers.
(async-start
(lambda ()
(delete-file "/ssh:remote:big-file.tar.gz"))
'ignore)The mechanism is that each child Emacs communicates over a process, and the output of that process is captured in an Emacs buffer. With a callback, the library knows when the work is done and can clean up. With nil, there is no completion signal, so the buffer stays. This is a small design detail with an outsized effect on a long running session. A user who fires off fifty background operations with a nil callback ends up with fifty stale buffers, and the symptom is a memory profile that grows over the day for reasons nobody can explain. The example also shows the case the library is really for. A file deletion on a remote host over TRAMP is exactly the operation that blocks the editor, and it is also the operation whose result nobody needs to look at.
Passing variables in, and messages flowing both ways
Because the child starts fresh with -Q, it has none of your variables. The library provides async-inject-variables to send them over.
(async-start
`(lambda ()
,(async-inject-variables "\\`user-mail-address\\'")
(format "Mail: %s" user-mail-address))
(lambda (result) (message "%s" result)))The backquoted lambda is the point. The unquote is where the variable is evaluated in the parent and its value is spliced into the source text that the child will evaluate. Without that step the child would try to look up user-mail-address in its own empty configuration and get the wrong answer. The second mechanism is bidirectional messaging, which the README describes as the parent and child exchanging messages while the child runs.
(let ((proc
(async-start
(lambda ()
(async-send :status "working")
(sleep-for 2)
(let ((msg (async-receive)))
(plist-get msg :data)))
(lambda (result)
(if (async-message-p result)
(message "Status: %s" (plist-get result :status))
(message "Done: %s" result))))))
(async-send proc :data "here you go"))The child sends a :status message and then waits with async-receive for the parent's answer, and the parent distinguishes a message from a final result with async-message-p. That is a genuine two way channel rather than a one shot function call, which is what you need for a job that asks questions while it runs. The third piece is async-let, which evaluates bindings asynchronously and runs the body when they are ready.
(async-let ((x (heavy-computation-1))
(y (heavy-computation-2)))
(message "x=%s y=%s" x y))Two independent slow computations run in parallel and the body waits for both, which is the shape a lot of Emacs code wants and which plain Lisp cannot express.
Four applications ship with the core library
The README separates the core from what is built on top of it, and the four bundled applications are what most people actually install the package for. dired-async is asynchronous file operations, copy, rename and symlink, in Dired. Its whole setup is one line.
(dired-async-mode 1)With that, those operations run in the background, and the README notes it works in Helm too if you use that, which matters because Helm is where a lot of people do their file operations. async-bytecomp byte-compiles packages in a clean subprocess. The README is specific about why: it avoids the stale-bytecode bugs you get when compiling in a running session, and it avoids the nasty bugs that come from the old version of a package still being loaded during compilation.
(async-bytecomp-package-mode 1)
(setq async-bytecomp-allowed-packages '(all))The allowed packages variable is a safety control rather than a formality, since compiling every installed package in the background is a heavy operation to enable by accident. async-package downloads and installs packages asynchronously, and smtpmail-async sends email in the background.
(setq message-send-mail-function 'async-smtpmail-send-it)That last one comes with a warning worth repeating. Send at least one email synchronously first, so any TLS negotiation or auth prompts happen interactively. After that, async sending works fine. The reason is obvious in hindsight: a child process with -Q has no way to prompt you for a password, so the first send has to be the one that establishes the credentials.
Auth sources, and the -q switch that TRAMP needs
Anything involving a remote host needs credentials the child can use without asking you, and the README points at auth-sources.
(setq auth-sources '("~/.authinfo.gpg" "~/.netrc"))Both files are read by the child, so a password prompt never appears in a subprocess where nobody can answer it. Then there is the exception, and it is the most useful single line in the README. On Emacs 26 and later, set async-quiet-switch to "-q" when using TRAMP, because there is a bug where -Q prevents auth-source from working properly. That is a precise, reproducible piece of knowledge and it explains the whole shape of the library. The child is started with -Q for hygiene, but -Q also means no init file, and in some Emacs versions that also means the auth-source machinery does not initialise. The workaround is to add -q, which suppresses the loading of the init file without changing the behaviour quite as drastically. A reader who hits a TRAMP operation hanging in a child process should look at this line before anything else. The Makefile in the repository shows how the project itself is built, and it runs Emacs in batch mode with -Q as well, which is a fitting detail: even the build of the library avoids loading any configuration.
Install paths, a Nix development shell and a quiet release line
The package is distributed through the usual Emacs channels: GNU ELPA, MELPA and MELPA Stable, with badges for all three in the README.
(package-install 'async)Debian and Ubuntu users can install it with apt-get install elpa-async, and building from source is make all && make install. For contributors there are two paths. If you have Nix installed, the flake gives you a development shell with all dependencies and a check command that runs everything.
nix develop # Enter development shell with all dependencies
nix flake check # Run all checks (compile, lint, format, tests)Otherwise the project uses Eask as its build tool, with a different target for each job.
make all # Clean, generate autoloads, compile
eask install-deps --dev
eask test buttercup # Run tests
make lint # Byte-compile with warnings-as-errors + package-lint
make format-check # Check code formatting
make format # Fix code formattingThe Makefile itself reveals how the package is put together. It compiles every file matching *async*.el in batch mode with an explicit load path of the package directory, generates the autoloads file with update-directory-autoloads, and defines clean, autoloads, batch-compile, install, uninstall, lint and test targets. There is a lefthook.yml for git hooks, a flake.nix with a flake.lock, an Eask directory, a tests/ directory and a CLAUDE.md file for coding agents. The licence is GPL version 3, with the COPYING file at the root and the Makefile header repeating the terms and the warranty disclaimer. On releases, the tags are v1.9.7 from 2022-09-29, v1.9.3 from 2018 and v1.9.1 from 2017. The last push was on 2026-07-29 and the repository is not archived, so the code is being worked on even though the published package versions move slowly. That gap is fine for a package this stable, and it is the reason the README's advice about Emacs versions and TRAMP behaviour matters more than the version number.
Editorial conclusion
Adopt emacs-async if your Emacs sessions stall on TRAMP file operations, package compilation or mail sending, and you want those jobs moved into a child process without rewriting them. Do not adopt it as a general concurrency framework, since it is specifically about running Emacs Lisp in a child Emacs started with -Q, and remember that the child has none of your configuration unless you inject it. Before you rely on it, set async-quiet-switch to "-q" if you use TRAMP on Emacs 26 or later, and use 'ignore rather than nil as the callback so you do not accumulate process buffers.
Frequently asked questions
What does emacs-async do?
It lets you run Emacs Lisp code in a child Emacs process started with -Q, so the child executes what you give it and passes the result back while your main Emacs never blocks. It is aimed at slow operations such as copying files over TRAMP, byte-compiling packages and sending mail.
How do I install emacs-async?
The package is on GNU ELPA and MELPA, so (package-install 'async) works. Debian and Ubuntu users can run apt-get install elpa-async, and building from source is make all && make install.
Why should the callback not be nil?
The README warns that if you do not care about the return value you should pass 'ignore as the callback, and not just leave it nil, because you will leak process buffers.
How do I pass variables to the child Emacs process?
Use async-inject-variables inside a backquoted lambda, so the variable is evaluated in the parent and its value is spliced into the code the child runs. The child starts with -Q and therefore has none of your settings.
What is the TRAMP and auth-source gotcha in emacs-async?
On Emacs 26 and later you should set async-quiet-switch to "-q" when using TRAMP, because of a bug where -Q prevents auth-source from working properly. The README also advises configuring auth-sources so the child process can authenticate without prompting you.
Official sources
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.
[](https://hysenlabs.com/projects/jwiegley-emacs-async)Was this useful?