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