I changed split-width-threshold from 160 to 120 in config-core.el. Ran M-x eval-buffer. Checked the value — still 160.
Tried again. Still 160. Stared at the screen. Opened a scratch buffer, typed (setq split-width-threshold 120), hit C-x C-e. Checked. 120. Works.
But the same line in config-core.el, evaluated via eval-buffer, doesn’t take effect?
Turns out: it did. The file sets the value to 160. I was editing it to 120 in my head, but the saved file still said 160. Classic. But the confusion led me down a rabbit hole that every Emacs user eventually falls into — why do some config changes need a restart, some need eval-buffer, some need cache deletion, and some just need M-: in the minibuffer?
The answer: Emacs has layers, like a geological formation. Each layer has its own caching and lifecycle rules. Once you understand what lives at each layer, the “when do I reload?” question answers itself.
Layer 1: Just eval the expression
The simplest case. You have a variable:
(setq split-width-threshold 120)
Variables in Emacs are global mutable state. Evaluating this form sets the value immediately in the running session. There’s no registration, no lifecycle, no caching. The variable just… changes.
How to apply: M-: in the minibuffer, C-x C-e at the end of the form, or select and M-x eval-region.
When this is enough: Any setq that doesn’t depend on a package being loaded first. Things like split-width-threshold, gc-cons-threshold, inhibit-startup-message. These are plain Emacs variables that exist from the moment Emacs starts.
The trap: If your setq is inside a use-package :config block, see Layer 3.
Layer 2: eval-buffer — re-run all top-level forms
M-x eval-buffer runs every top-level form in the current buffer, top to bottom. It’s like replaying the file as a script. This works great — until it doesn’t.
When it works perfectly:
;; Pure settings --- idempotent, no side effects
(setq org-src-fontify-natively t)
(setq org-src-tab-acts-natively t)
Run this ten times, same result every time. No harm done.
When it gets weird:
;; Hooks accumulate
(add-hook 'dired-mode-hook 'auto-revert-mode)
Every eval-buffer adds another copy of auto-revert-mode to the hook. The hook list grows: (auto-revert-mode auto-revert-mode auto-revert-mode ...). Functionally it’s fine — enabling a mode three times does the same thing as once. But it’s sloppy, and some hooks aren’t idempotent.
When it causes real problems:
;; Keybindings --- old binding doesn't unbind
(global-set-key (kbd "C-c s") #'hydra-search/body)
If you change this to bind C-c s to something else, eval-buffer will set the new binding. But if you remove the line entirely and re-eval, the old binding persists in the running session. eval-buffer runs what’s there — it doesn’t know what used to be there.
The mental model: eval-buffer is additive. It applies what’s in the file. It doesn’t unapply what’s no longer in the file.
Layer 3: use-package and deferred loading
This is where most people get confused. Here’s a typical declaration:
(use-package magit
:defer t
:bind ("C-x g" . magit-status)
:config
(setq magit-display-buffer-function
#'magit-display-buffer-same-window-except-diff-v1))
use-package splits configuration into phases:
| Section | When it runs | Re-eval behavior |
|---|---|---|
:init |
Immediately, during eval-buffer |
Runs again every time you eval |
:bind |
Immediately (sets up autoload) | Runs again |
:hook |
Immediately (registers hook) | Runs again (may double-register) |
:config |
Once, when the package first loads | Does NOT re-run if package already loaded |
:custom |
Immediately via customize-set-variable |
Runs again |
That :config behavior is the one that bites. When you eval config-core.el during a session, magit is already loaded. use-package sees that and skips the :config block entirely. Your changes to settings inside :config are invisible.
Example: You add this to magit’s :config:
:config
(setq magit-log-margin '(t "%Y-%m-%d" magit-log-margin-width t 18))
Re-eval the buffer. Nothing changes. Magit was already loaded, so :config didn’t run.
Workaround: Pull the setq out and eval it directly with M-:. Or wrap it in with-eval-after-load outside of use-package:
(with-eval-after-load 'magit
(setq magit-log-margin '(t "%Y-%m-%d" magit-log-margin-width t 18)))
This re-evaluates every time because the package is already loaded, so the body runs immediately.
The mental model: use-package :config is “run once on first load.” If the package is loaded, that ship has sailed.
Layer 4: Restart Emacs
Some things only happen once, at startup. No amount of eval-ing will redo them.
early-init.el runs before the GUI frame exists. It’s where you do things like:
;; Disable UI elements before the frame is created
(push '(menu-bar-lines . 0) default-frame-alist)
(push '(tool-bar-lines . 0) default-frame-alist)
;; Maximize GC during startup
(setq gc-cons-threshold most-positive-fixnum)
These modify default-frame-alist, which is read once during frame creation. Eval-ing them after startup pushes values into the list, but the frame already exists. The horse has left the barn.
package-initialize builds the list of available packages from your package-archives. It runs once during init. If you change your archive list and eval it, the package system doesn’t re-scan. You need M-x package-refresh-contents or a restart.
Load order dependencies: If config-programming depends on something set up in config-core, and you only re-eval config-programming, it picks up whatever state config-core left behind — which might be stale if you changed config-core but didn’t re-eval it first.
require caching: Emacs tracks which files have been require’d. Once (require 'config-core) succeeds, calling it again is a no-op — Emacs checks features, sees config-core is already there, and skips the load entirely. This is why load-config-module in my init.el uses require — good for startup performance, bad for reloading.
;; This does nothing the second time:
(require 'config-core)
;; This always reloads:
(load "config-core")
The mental model: Restart is the nuclear option. It gives you a clean slate — fresh features list, fresh frame, fresh load order. When in doubt, restart.
Layer 5: Byte-compiled cache (.elc)
Emacs can compile .el files to bytecode (.elc). Byte-compiled files load faster. But here’s the thing: when both config-core.el and config-core.elc exist, Emacs loads the .elc.
If you edit config-core.el but forget to recompile, Emacs happily loads the stale .elc with your old code. Your changes are sitting right there in the .el file, plain as day, and Emacs is ignoring them.
How to check:
# Find all .elc files in your config
find ~/.emacs.d/lisp/ -name "*.elc"
How to fix:
# Delete all byte-compiled files
find ~/.emacs.d/lisp/ -name "*.elc" -delete
# Or recompile
emacs --batch -f batch-byte-compile lisp/config-core.el
When this bites you: It’s sneaky because most Emacs configs don’t manually byte-compile. But some packages do it during installation, and Doom Emacs compiles aggressively. If you used Doom before (guilty) and migrated to vanilla, leftover .elc files from the Doom era can haunt you.
The mental model: .elc is a compiled cache. Like any cache, it can go stale. The .el file is the source of truth.
Layer 6: Native compilation cache (.eln)
Emacs 28+ introduced native compilation — compiling Elisp to actual machine code via libgccjit. It’s faster than bytecode, but it adds another cache layer.
Native-compiled files live in eln-cache/ (configured in early-init.el):
(add-to-list 'native-comp-eln-load-path
(expand-file-name "eln-cache/" user-emacs-directory))
The compilation happens asynchronously in the background. You change a file, Emacs eventually recompiles it to .eln — but “eventually” might mean “after you’ve already loaded the old version.”
The loading priority:
.eln(native compiled) — fastest, loaded first if available.elc(byte compiled) — loaded if no.eln.el(source) — loaded if neither compiled version exists
So a stale .eln shadows a fresh .elc, which shadows a fresh .el. It’s caches all the way down.
How to fix:
# Find your eln-cache
find ~/.emacs.d/eln-cache/ -name "*.eln" | head
# Nuclear option: delete all native-compiled cache
rm -rf ~/.emacs.d/eln-cache/
Emacs will recompile everything in the background on next startup. Takes a minute or two but gives you a clean slate.
When this bites you: Rarely, in practice. Native compilation is smart enough to check timestamps in most cases. But if you’re doing something exotic — like replacing a package’s .el file in place without going through the package manager — the .eln cache can absolutely lie to you.
Layer 7: Package autoloads
When you install a package, Emacs generates an autoloads file — a manifest of which functions should trigger loading which package. This is what makes :bind and :commands in use-package work:
(use-package magit
:bind ("C-x g" . magit-status) ; Creates autoload: pressing C-x g loads magit
:commands (magit-status magit-diff)) ; These also create autoloads
When you press C-x g, Emacs looks up magit-status in the autoloads, sees it lives in the magit package, loads magit, then calls the function.
The problem: Autoloads are generated at package install time. If a package adds a new interactive command in an update, but you don’t reinstall/update the package, the autoload file doesn’t know about the new command. Your :commands declaration for the new function silently does nothing.
More commonly: If you write your own function in a package’s namespace and try to autoload it:
(use-package magit
:commands (magit-status my/magit-custom-diff)) ; my/ function isn't in magit's autoloads
my/magit-custom-diff won’t be autoloaded because it doesn’t exist in magit’s autoload file. You’d need to define it separately and either autoload it manually or put it in :init (which runs immediately).
How to regenerate:
;; After updating packages:
M-x package-quickstart-refresh
;; Or delete and reinstall the package
The mental model: Autoloads are a static index generated at install time. They don’t auto-update when package contents change.
The decision tree
Okay, you changed something. What do you need to do?
| I changed… | Do this |
|---|---|
A setq for a built-in variable |
M-: or C-x C-e on that one form |
A setq inside use-package :config (package already loaded) |
M-: the setq directly — re-eval won’t re-trigger :config |
A setq inside use-package :config (package not yet loaded) |
Re-eval is fine — :config hasn’t run yet, will run on load |
A use-package :init or :custom block |
eval-buffer works |
A use-package :hook |
eval-buffer works but may double-register — restart to be safe |
| A keybinding (added or changed) | eval-buffer for the new binding |
| A keybinding (removed) | Restart — eval can’t unbind what’s no longer in the file |
early-init.el (frame, GC, native-comp) |
Restart Emacs |
Package archives or package-initialize |
M-x package-refresh-contents or restart |
| Load order between modules | Restart |
| Anything, but changes aren’t showing up | Check for stale .elc / .eln caches |
Or, as a flowchart in your head:
- Is it a simple variable? Eval just that form. Done.
- Is it inside
:configof an already-loaded package? Eval thesetqdirectly, not theuse-packageform. - Are you removing something? Restart. Eval is additive.
- Is it an early-init or startup-only thing? Restart.
- Did eval-buffer not work and you can’t figure out why? Check for
.elc/.elncache. Delete them. Restart.
Why it’s designed this way
This isn’t bad design. It’s the consequence of a system that’s been running continuously since 1976, where:
- Performance matters — deferred loading, compilation caches, and autoloads exist because loading everything eagerly would make startup take 30 seconds
- State is mutable — Emacs is a Lisp image. Variables, hooks, keymaps are all live mutable objects in memory. There’s no “declarative config” that gets diffed and applied
- Backward compatibility is sacred —
requirecaching,.elcpriority,eval-after-loadsemantics all exist because changing them would break decades of Elisp packages
The upside: you can change almost anything in a running Emacs without restarting. The downside: you have to understand which layer you’re touching to know how.
Once you internalize the layers, it stops feeling arbitrary. Every “weird reload behavior” traces back to one of these seven mechanisms. The mystery dissolves into mechanics.