Auto-Update
Keeping Electron users current — autoUpdater vs electron-updater, update feeds, the update lifecycle, staged rollout, differential downloads, and graceful failure handling
Auto-Update
Auto-update is not a convenience feature — it is a security obligation. Every Electron app embeds a full Chromium, and Chromium ships security fixes constantly. An app that cannot update its users is an app that strands them on a vulnerable browser engine. A reliable update path is part of shipping responsibly.
autoUpdater vs electron-updater
Electron has a built-in autoUpdater module backed by Squirrel.Mac and Squirrel.Windows. It works but is bare: it needs a hosted update server with a specific API, has no Linux support, and gives you little control.
electron-updater (part of the electron-builder ecosystem) is the common choice. It reads the metadata files electron-builder already produces, supports many providers including plain HTTP, and works on Windows, macOS, and Linux AppImage.
Built-in autoUpdater | electron-updater | |
|---|---|---|
| Backend | Squirrel.Mac / Squirrel.Windows | Reads latest*.yml from any feed |
| Providers | Squirrel-compatible server only | GitHub, generic HTTP(S), S3, Spaces, Keygen |
| Linux | Not supported | AppImage supported |
| Differential downloads | No | Yes (NSIS + blockmap) |
| Channels / staged rollout | Manual | Built in |
| Pairs with | Any packager | electron-builder |
Use electron-updater for most apps. It pairs directly with electron-builder’s output, runs on all three platforms, supports generic and self-hosted feeds, and gives you channels and differential downloads. Reach for the built-in autoUpdater only if you have an existing Squirrel server you must keep.
Update Feeds and Metadata
electron-updater checks an update feed — a location that hosts your artifacts plus small metadata files. electron-builder generates the metadata at build time when you set a publish provider (see the Packaging page):
| File | Platform | Contents |
|---|---|---|
latest.yml | Windows | version, file names, sha512, release date |
latest-mac.yml | macOS | same, for the zip/dmg |
latest-linux.yml | Linux | same, for the AppImage |
*.blockmap | Win/mac | chunk hashes enabling differential download |
Common providers:
# electron-builder.yml — pick one provider
publish:
provider: github # GitHub Releases (private repos need a token)
owner: example
repo: myapp
# or a generic HTTPS server you control:
# publish:
# provider: generic
# url: https://downloads.example.com/myapp
# or S3:
# publish:
# provider: s3
# bucket: myapp-releasesThe updater fetches latest.yml, compares its version to the running app, and if newer downloads the artifact named in the metadata — verifying the sha512 before applying.
Code Signing Is a Prerequisite
Auto-update is built on top of code signing. An unsigned update will not apply.
- macOS verifies that the update is signed by the same Developer ID as the installed app, and Squirrel.Mac applies updates from the
ziptarget (so your build must produce a signed zip, not only a dmg). - Windows verifies the Authenticode signature of the new installer.
Unsigned macOS builds silently fail to update. Squirrel.Mac will download the update and then refuse to apply it because the code signature does not validate — often with no visible error. Developers test on an unsigned local build, see "update-downloaded" fire, assume it works, and ship. It does not. You must sign and notarize, and emit the zip target, for macOS auto-update to function.
The Update Lifecycle
electron-updater emits a sequence of events. Wire them in the main process and forward state to the renderer over your IPC bridge so the UI can prompt the user.
// main/updater.ts
import { autoUpdater } from 'electron-updater';
import log from 'electron-log/main';
import { dialog, BrowserWindow } from 'electron';
export function initAutoUpdate(getWindow: () => BrowserWindow | null) {
autoUpdater.logger = log;
autoUpdater.autoDownload = true; // download in the background
autoUpdater.autoInstallOnAppQuit = true; // apply on next quit if not prompted
autoUpdater.on('checking-for-update', () => log.info('checking for update'));
autoUpdater.on('update-available', (info) => {
log.info('update available', info.version);
// download is already underway because autoDownload = true
});
autoUpdater.on('update-not-available', () => log.info('on latest version'));
autoUpdater.on('download-progress', (p) => {
getWindow()?.webContents.send('update:progress', {
percent: p.percent,
bytesPerSecond: p.bytesPerSecond,
});
});
autoUpdater.on('update-downloaded', async (info) => {
const win = getWindow();
if (!win) return;
const { response } = await dialog.showMessageBox(win, {
type: 'info',
buttons: ['Restart now', 'Later'],
defaultId: 0,
cancelId: 1,
message: `Version ${info.version} is ready`,
detail: 'Restart to apply the update.',
});
if (response === 0) autoUpdater.quitAndInstall();
// else: autoInstallOnAppQuit applies it on next normal quit
});
autoUpdater.on('error', (err) => {
log.error('update error', err);
// never crash the app on an update failure — see below
});
// Check on startup, then periodically. Do NOT check before app is ready.
autoUpdater.checkForUpdatesAndNotify();
setInterval(() => autoUpdater.checkForUpdates(), 6 * 60 * 60 * 1000);
}The key events, in order:
| Event | Meaning |
|---|---|
checking-for-update | A check started |
update-available | A newer version exists; download begins (if autoDownload) |
update-not-available | Already current |
download-progress | Periodic progress (percent, bytesPerSecond) |
update-downloaded | Ready to install via quitAndInstall() |
error | Something failed — handle, do not throw |
Staged and Channel-Based Rollout
Do not ship a new version to 100% of users at once. Two complementary mechanisms:
Staging percentage — gate the rollout to a fraction of users. electron-builder writes a stagingPercentage into the metadata; the updater hashes a stable per-install GUID against it, so a given machine consistently is or isn’t in the rollout.
# latest.yml (generated; can be edited to ramp a rollout)
version: 1.4.0
stagingPercentage: 25 # only ~25% of installs will pick this up
files:
- url: MyApp-1.4.0.exe
sha512: ...Channels — ship latest, beta, and alpha tracks. Opt some users into pre-release channels and promote a build to latest once it’s proven.
autoUpdater.channel = 'beta'; // this install follows the beta track
autoUpdater.allowPrerelease = true; // accept beta/alpha tagsRamp stagingPercentage (5% → 25% → 50% → 100%) while watching crash and error rates; halt or roll back if metrics regress.
Differential Downloads
Re-downloading a 120 MB app for a one-line fix wastes bandwidth and time. electron-updater supports differential updates: the .blockmap file maps the installer into content-addressed chunks, and the client downloads only the chunks that changed since its current version.
- Windows NSIS and macOS differential updates work out of the box when blockmaps are present in the feed.
- The first install is always full; subsequent updates can be differential.
- Keep old blockmaps available on the feed so clients on older versions can diff against them.
This typically cuts a patch download to a few megabytes.
Linux Caveats
Linux update behavior depends on the package format:
- AppImage — electron-updater updates it in place; works well.
- deb / rpm — these are owned by the system package manager (
apt,dnf). The idiomatic path is to publish to your own apt/yum repo and let the OS update; don’t self-update a deb from inside the app. - Snap / Flatpak — the store and its daemon handle updates automatically; your app should not attempt to self-update.
Detect the format at runtime (e.g. process.env.APPIMAGE) and only run the in-app updater for AppImage.
Failure Handling
Updates run over the network against external infrastructure — they will fail sometimes. The cardinal rule: an update failure must never take down the app.
autoUpdater.on('error', (err) => {
log.error('update failed; will retry later', err);
// Do NOT rethrow, do NOT quit, do NOT show a blocking modal on every check.
scheduleRetryWithBackoff(); // e.g. 5m → 15m → 1h, capped
});- Retry with backoff. A transient network blip should not mean "no updates until restart." Back off and try again later.
- Degrade gracefully. If the feed is unreachable, the app keeps running on its current version. Surface a quiet "couldn’t check for updates" state at most — never an error wall.
- Don’t relaunch without consent. For a foreground app, never call
quitAndInstall()out from under the user. Prompt, or defer toautoInstallOnAppQuitso the update applies on a quit the user initiated. Forcing a restart mid-task loses work and erodes trust.
Test the real, signed update path before relying on it. Mock-testing the events is not enough. Build two signed, notarized versions, host them on a staging feed, install the older one, and confirm it actually updates to the newer one on each OS. Auto-update is the one feature you cannot hotfix if it’s broken — by definition, a broken updater cannot ship its own fix.
Checklist
- Use
electron-updaterwith an electron-builderpublishprovider that emitslatest*.yml. - Sign and notarize macOS, emit the
ziptarget — unsigned updates silently fail. - Wire the lifecycle events in main; forward progress to the renderer; prompt before
quitAndInstall(). - Roll out with
stagingPercentageand/or channels; ramp while watching error rates. - Keep blockmaps on the feed for differential downloads.
- Run the in-app updater only for AppImage on Linux; leave deb/rpm/snap/flatpak to the system.
- Handle
errorwith retry/backoff; never crash, block, or force-relaunch on failure. - Verify the full signed update path on every platform before release.