Elfeed: Emacs RSS Reader
Table of Contents
1. Overview
Elfeed is a feed reader for Emacs. It fetches RSS/Atom feeds, stores entries in an in-memory database backed by disk, and provides a search-first interface inspired by notmuch.
Key properties:
- Pure Emacs Lisp implementation
- SQLite-like on-disk database format (custom, not SQLite)
- Incremental search with tag filters
- Extensible via hooks and advice
The entry point is M-x elfeed. The database lives in ~/.elfeed/.
2. Database Architecture
Elfeed stores feed state in three files:
| File | Purpose |
|---|---|
~/.elfeed/index |
Entry metadata (GUID, timestamp, tags) |
~/.elfeed/data/XX/YY/GUID |
Entry content (title, author, body HTML) |
~/.elfeed/db.el |
Feed URLs and metadata (read-only after init) |
The index is a hash table serialized with print=/=read. Entry content is stored in a two-level directory tree keyed by GUID hash prefix.
2.1. Inspecting the Database
;; Load elfeed database without starting the UI (require 'elfeed-db) (elfeed-db-load) ;; Count total entries (hash-table-count elfeed-db-entries)
The database is lazy-loaded. Reading an entry from disk occurs on first access:
;; Fetch an entry by GUID (triggers disk read if not in memory) (let ((entry (elfeed-db-get-entry "https://example.com/post-123"))) (when entry (list :title (elfeed-entry-title entry) :date (elfeed-entry-date entry) :tags (elfeed-entry-tags entry))))
3. Feed Configuration
Feeds are registered in elfeed-feeds, a list of URLs or (url . tags) pairs.
3.1. Basic Configuration
(setq elfeed-feeds '("https://nullprogram.com/feed/" "http://planet.emacsen.org/atom.xml" ("https://openai.com/blog/rss.xml" ai research)))
The third entry assigns tags ai and research to all entries from that feed.
3.2. Hierarchical Organization with elfeed-org
The elfeed-org package allows feed lists in org-mode syntax with nested tags:
* Blogs :elfeed: ** entry-title: \(linux\|linus\|ubuntu\|kde\|gnome\) :linux: ** http://git-annex.branchable.com/design/assistant/blog/index.rss :mustread: ** Software Development :dev: *** Emacs :emacs:mustread: **** http://www.terminally-incoherent.com/blog/feed **** http://nullprogram.com/feed **** http://planet.emacsen.org/atom.xml *** AI & Machine Learning :ai: **** https://openai.com/blog/rss.xml **** https://distill.pub/rss.xml :mustread:
Tags propagate down the tree. The nullprogram.com feed inherits elfeed, dev, emacs, mustread.
Configuration:
(require 'elfeed-org) (elfeed-org) (setq rmh-elfeed-org-files '("~/.emacs.d/elfeed.org"))
3.3. Title Filtering
The entry-title: pattern applies tags based on regex match against entry titles:
** entry-title: \(emacs\|org-mode\) :emacs:
Any entry with "emacs" or "org-mode" in the title receives the emacs tag, regardless of source feed.
4. Search Interface
The elfeed buffer is a filtered view of the database. The search bar accepts space-separated tokens.
4.1. Search Syntax
| Query | Matches |
|---|---|
@6-months-ago |
Entries from last 6 months |
+unread |
Entries tagged unread |
-junk |
Entries NOT tagged junk |
emacs lisp |
Title/content contains both words (case-insensitive) |
@1-week-ago +unread emacs |
Unread Emacs entries from last week |
Default filter is @6-months-ago +unread.
4.2. Programmatic Search
;; Find all entries tagged 'ai' from the last month (elfeed-search-set-filter "@1-month-ago +ai")
The filter is a string parsed by elfeed-search-parse-filter.
5. Entry Actions
| Key | Action |
|---|---|
RET |
Open entry (eww or external browser) |
b |
Open in browser |
y |
Copy URL to clipboard |
r |
Mark as read (remove unread tag) |
u |
Mark as unread (add unread tag) |
+ |
Add tag |
- |
Remove tag |
g |
Refresh all feeds |
G |
Fetch feed updates (background) |
5.1. Custom Actions
Add custom keybindings via elfeed-show-mode-map:
(defun my/elfeed-pocket-save () "Save current entry to Pocket." (interactive) (let ((url (elfeed-entry-link elfeed-show-entry))) (shell-command (format "pocket-cli add %s" (shell-quote-argument url))) (message "Saved to Pocket: %s" url))) (define-key elfeed-show-mode-map (kbd "P") 'my/elfeed-pocket-save)
6. Fetch Strategy
Elfeed fetches feeds in parallel. Default concurrency is 8.
(setq elfeed-curl-max-connections 8)
Fetch interval is manual by default. To auto-update every 10 minutes:
(run-at-time nil (* 10 60) 'elfeed-update)
6.1. Fetch Hooks
Hook elfeed-new-entry-hook runs for each new entry:
(add-hook 'elfeed-new-entry-hook
(lambda (entry)
(when (string-match-p "breaking" (elfeed-entry-title entry))
(message "ALERT: %s" (elfeed-entry-title entry)))))
7. Performance Characteristics
Database size grows linearly with entry count. On a corpus of 4,283 entries:
| Metric | Value |
|---|---|
| Disk usage | 47 MB |
| Load time (cold) | 1.2 seconds |
| Load time (warm) | 0.3 seconds |
| Search latency | < 50ms |
Entry content is HTML. Viewing an entry in elfeed-show mode renders it via shr (Simple HTML Renderer).
7.1. Pruning Old Entries
Limit database growth by removing entries older than N days:
(defun my/elfeed-prune-old-entries () "Remove entries older than 90 days." (interactive) (let* ((cutoff-time (time-subtract (current-time) (days-to-time 90))) (entries (elfeed-db-get-all-entries)) (count 0)) (dolist (entry entries) (when (time-less-p (elfeed-entry-date entry) cutoff-time) (elfeed-db-remove-entry entry) (setq count (1+ count)))) (elfeed-db-save) (message "Removed %d old entries" count)))
Run monthly via cron or run-at-time.
8. Integration Patterns
8.1. Org-mode Capture
Capture interesting articles to org-mode:
(defun my/elfeed-org-capture () "Capture current entry to org-mode." (interactive) (let* ((entry elfeed-show-entry) (title (elfeed-entry-title entry)) (url (elfeed-entry-link entry)) (date (format-time-string "%Y-%m-%d" (elfeed-entry-date entry)))) (org-capture nil "a") ; assuming template key 'a' (insert (format "* [[%s][%s]]\n:PROPERTIES:\n:CREATED: %s\n:END:\n\n" url title date)))) (define-key elfeed-show-mode-map (kbd "c") 'my/elfeed-org-capture)
8.2. Scoring and Ranking
Tag entries by predicted interest:
(defun my/elfeed-score-entry (entry) "Assign a score tag based on content." (let ((title (elfeed-entry-title entry)) (tags (elfeed-entry-tags entry))) (cond ((and (member 'ai tags) (string-match-p "llm\\|transformer" title)) (elfeed-tag entry 'high-priority)) ((string-match-p "javascript" title) (elfeed-tag entry 'low-priority)) (t nil)))) (add-hook 'elfeed-new-entry-hook 'my/elfeed-score-entry)
Search for high-priority items: +high-priority +unread.
8.3. Export to JSON
Extract feed data for external tools:
(defun my/elfeed-export-json (file) "Export all unread entries to JSON." (interactive "FExport to: ") (require 'json) (let* ((entries (elfeed-search-parse-filter "+unread")) (data (mapcar (lambda (e) `((title . ,(elfeed-entry-title e)) (url . ,(elfeed-entry-link e)) (date . ,(format-time-string "%Y-%m-%dT%H:%M:%S" (elfeed-entry-date e))) (tags . ,(mapcar 'symbol-name (elfeed-entry-tags e))))) entries))) (with-temp-file file (insert (json-encode data))) (message "Exported %d entries to %s" (length entries) file)))
9. Comparison with Alternatives
| Feature | Elfeed | Newsboat | Feedly |
|---|---|---|---|
| Platform | Emacs | Terminal | Web |
| Database | Custom disk format | SQLite | Cloud |
| Search | Tag-based, regex | SQL-like | AI-powered |
| Offline | Yes | Yes | No |
| Extensibility | Elisp hooks | Shell scripts | Browser extensions |
Elfeed fits the Emacs philosophy: live in one environment, extend via Lisp.
10. Troubleshooting
10.1. Feed Not Updating
Check elfeed-log buffer for fetch errors:
(switch-to-buffer "*elfeed-log*")
Common causes:
- 403/404 errors (feed moved or removed)
- SSL certificate issues (
elfeed-curl-extra-argumentscan disable verification) - Timeout (increase
url-queue-timeout)
10.2. Database Corruption
If the index file is corrupted, rebuild from content:
# Backup current database cp -r ~/.elfeed ~/.elfeed.backup # Remove index rm ~/.elfeed/index # Restart Emacs and run elfeed # Index will rebuild from data/ directory
Rebuilding is slow (minutes for thousands of entries) but preserves all content.
11. Source Code Architecture
Elfeed is distributed as three packages:
| Package | Lines of Code | Purpose |
|---|---|---|
elfeed |
2,100 | Core database and UI |
elfeed-search |
500 | Search buffer and filters |
elfeed-show |
400 | Entry display |
The database module (elfeed-db.el) is independent of UI. Command-line tools can load it directly.
11.1. Custom Feed Parser
Override default parsers for non-standard feeds:
(defun my/custom-feed-parser (xml) "Parse a custom XML format." ;; Return list of entries in elfeed format (list (elfeed-entry--create :title "Example Title" :link "https://example.com" :date (current-time) :content "<p>Content</p>"))) (add-to-list 'elfeed-feed-functions '("https://custom-feed.example.com/rss" . my/custom-feed-parser))
12. Configuration Reference
Complete minimal configuration:
(use-package elfeed :ensure t :config (setq elfeed-db-directory "~/.elfeed" elfeed-curl-max-connections 8 elfeed-search-filter "@6-months-ago +unread" elfeed-feeds '(("https://nullprogram.com/feed/" emacs) ("https://openai.com/blog/rss.xml" ai) ("http://planet.emacsen.org/atom.xml" emacs community)))) (use-package elfeed-org :ensure t :after elfeed :config (elfeed-org) (setq rmh-elfeed-org-files '("~/.emacs.d/elfeed.org"))) ;; Keybinding (global-set-key (kbd "C-x w") 'elfeed) ;; Auto-update every 30 minutes (run-at-time nil (* 30 60) 'elfeed-update)
13. Further Reading
- github.com/skeeto/elfeed — Source repository
- github.com/remyhonig/elfeed-org — Org-mode feed lists
- Introducing Elfeed, an Emacs Web Feed Reader — Author's introduction
- elfeed README — Official documentation