turndown was the most-starred of the four tested libraries at the metadata snapshot, with 11,386 GitHub stars. On the four shared HTML fixtures it produced zero Markdown tables with its core defaults and used 24.6% more tokens than markdownify for the same inputs.
Every one of the four recovered every content probe. The differences are entirely in what happens to structure, and in what that structure costs you downstream.
What was measured, and on whose fixtures
This research base already had a pack for markitdown with five HTML fixtures and pre-registered probe strings — exact strings checked for survival, plus boilerplate strings used to detect page chrome. The aggregate comparison uses the four fixtures shared by all four converters: a bookshop catalogue, a quotes site, a hockey-statistics table, and the Wikipedia article on web scraping. A fifth fixture, Nothing but tables, is used only as a table-heavy diagnostic and is excluded from the 24.6% aggregate.
The four: turndown 7.2.4 (Node), markdownify 1.2.3, html2text 2025.4.15, and markitdown, whose published rows are used as-is. Pages: a bookshop catalogue, a quotes site, a hockey-statistics table, and the Wikipedia article on web scraping.
Every metric name below matches markitdown's own artifact field for field, so the rows sit next to each other without anyone reconciling two definitions of the same word.
The table

| Converter | Body probes | Output chars | Tokens (o200k) | Bytes/token | Markdown table rows | Links |
|---|---|---|---|---|---|---|
| turndown | 16/16 | 95,188 | 26,236 | 3.63 | 0 | 611 |
| markdownify | 16/16 | 76,868 | 21,062 | 3.65 | 36 | 599 |
| html2text | 16/16 | 76,452 | 21,176 | 3.61 | 32 | 545 |
| markitdown | 16/16 | 76,995 | 21,336 | 3.61 | 36 | 598 |
Four fixtures — the ones markitdown also ran. Full per-fixture numbers in fiveway-scores.json. Tokens counted with o200k_base; markitdown's table rows recounted from its own stored Markdown with the same counter used on the others.
Content survival is a tie. All sixteen body probes across the four fixtures survived every converter. If your only question is "will the text make it through", any of these four answers yes.
Structure is not a tie. On the four shared fixtures, markdownify and markitdown both emit 36 Markdown table rows; html2text emits 32; turndown emits none. In the separate table-only diagnostic, markdownify emitted 62 rows and html2text 59; those rows are not included in the aggregate above.
Token cost is 24.6% higher for turndown, and the reason is not the tables. I assumed it was, and the per-fixture numbers say otherwise — see below.
What turndown does to a table
Here is the hockey-statistics fixture, the same rows, three ways.
turndown:

Team Name
Year
Wins
Losses
Boston Bruins
1990
44
24
markdownify:
| Team Name | Year | Wins | Losses | ... |
| --- | --- | --- | --- | --- |
| Boston Bruins | 1990 | 44 | 24 | ... |
html2text:
Team Name | Year | Wins | Losses | ...
---|---|---|---|---
Boston Bruins | 1990 | 44 | 24 | ...
Every value survives turndown's conversion, which is why it scores 16/16 on the probes. What does not survive is which column each value belongs to. Read the turndown output and 44 is just a number on a line; you cannot recover that it is Boston's win count without counting positions and hoping no cell was empty. In this fixture some cells are empty, so counting positions does not work either.
For a model reading the output, that is the difference between a table it can answer questions about and a list of numbers it will guess at.
This is not a defect so much as a documented boundary — turndown's core does not handle tables, and turndown-plugin-gfm exists to add them. But the default install does not include it, and 11,386 stars suggests a lot of people are using the default.
Where the token gap actually comes from
I wrote the paragraph above believing the 24.6% token gap was the flattened tables showing up as characters. Then I looked at it per fixture, and it is not.
| Fixture | turndown tokens Ă· markdownify tokens |
|---|---|
| Quotes site (no tables) | 0.99Ă— |
| Bookshop catalogue | 1.06Ă— |
| Hockey statistics (one big table) | 1.37Ă— |
| Wikipedia (mostly prose, 9 table rows) | 1.29Ă— |
| Nothing but tables | 0.78Ă— |
On the fixture that is nothing but tables, turndown is 22% cheaper — because pipe scaffolding costs tokens too, and turndown emits none of it. Flattening a table is not, on its own, a token penalty.
The Wikipedia fixture is 74% of the total, and it has nine table rows. Its 15,378-character gap cannot be tables. It is this:
(function(){var className="client-js vector-feature-language-in-header-enabled…
.mw-parser-output cite.citation{font-style:inherit;word-wrap:break-word}…
(RLQ=window.RLQ||[]).push(function(){mw.config.set({"wgHostname":"mw-web…
turndown does not strip <script> and <style> content. markdownify and html2text both do. Counted by markers that only occur inside those elements: turndown's output carries 10 script markers and 84 style markers across the fixtures; the other two carry zero of each. On the Wikipedia page, eight lines of MediaWiki's inline JavaScript config and CSS account for 14,644 characters — 95% of the entire gap (script-style-stripping.json).
That is the finding I would actually act on. A flattened table is a structure problem you can see. A JavaScript config blob in your Markdown is pure cost with no information in it at all, and on a real-world page it dwarfs everything else in this comparison.
html2text writes tables you might not recognise
html2text scored 32 rows where markdownify and markitdown scored 36, and the first version of my counter scored it at 1.
That was my counting rule, not the library. html2text emits Team Name | Year | Wins without leading and trailing pipes — a common Markdown table form, but invisible to a regex that requires ^\|.*\|$. I had written that regex, run it, and been ready to report that html2text does not do tables.
It does. The corrected counter uses a heuristic: a run of consecutive pipe-containing lines with a separator row inside it. With that rule, html2text goes from 1 to 32. This is not a full Markdown parser, so the row totals should be read as measurements under a documented counter rather than universal rendering results.
Worth knowing if you post-process the Markdown with your own regex: two of these four libraries emit outer pipes and one does not.
The licence nobody mentions
| Converter | Licence | Install | Cold import | Stars | Last release |
|---|---|---|---|---|---|
| turndown | MIT | 3 npm packages, 8.8 MiB | 0.056 s | 11,386 | 2026-04-03 |
| markdownify | MIT | 5 packages, 1.8 MiB | 0.046 s | 2,235 | 2026-06-30 |
| html2text | GPL-3.0-or-later | 1 package, 0.2 MiB | 0.077 s | 2,168 | 2025-04-15 |
| markitdown | See its package metadata | Not measured in this install run | Not measured | — | — |
install-and-import.json. Each library installed into its own empty environment. Licences confirmed from three places: the registry metadata, the GitHub repo, and the installed package's own METADATA file, which reads License-Expression: GPL-3.0-or-later.
The lightest library in this install comparison — one package, 0.2 MiB — is GPL-3.0-or-later. Whether that affects a project depends on how the software is combined and distributed. Treat it as a selection checkpoint for whoever owns licensing; this article is not legal advice. markitdown is shown as unmeasured here because its install and licence were not captured by this particular artifact.
That trade is easy to miss because the licence is the one property that does not show up in a benchmark.
All three are dramatically lighter than the article-extraction libraries in the same category — those run 21 to 70 MiB. A converter is a much smaller thing than an extractor, and worth separating in your dependency budget.
Two confounds I had to fix before this table was true
The numbers above are the third version. The first two were wrong in ways worth naming, because both are easy to reproduce.
Five fixtures against four. markitdown ran four of these five files; the other three ran all five. Totalling each tool over its own set gave markdownify 98 table rows against markitdown's 36 and made it look like a capability gap. Over the same four, it is 36 against 36 — an exact tie. The fifth fixture is the table-heavy one, so the confound ran in the worst possible direction, inflating the newcomers against the incumbent by nearly threefold.

Two counters, one column. markitdown's published md_table_rows came from its own code, which I had not read. Comparing that number against mine could have been comparing two counters rather than two converters. Its Markdown output is stored on disk, so the fix was to run one counter over all four — and when I did, markitdown's recount came out at exactly its published figure per fixture (0, 0, 27, 9). The definitions agreed; I just could not have known that without checking.
Neither error would have been visible in the output. Both would have produced a confident, wrong table.
Who should use what
Feeding a model, or storing structured content from pages like these? Start with markdownify. It ties markitdown on emitted table-row count under this counter, sits in the lowest-token cluster, is MIT, and installs in 1.8 MiB. Validate it on your own page shapes before standardising.
Dependency budget measured in kilobytes, and you are not distributing? html2text. One package, 0.2 MiB, tables intact. Check the GPL question first, and note the last release was April 2025.
Already in a Node stack? turndown, with turndown-plugin-gfm installed alongside it — and strip <script> and <style> from the HTML before you hand it over, because turndown will not. Those two omissions cost a quarter more tokens and your table structure, and neither is visible unless you look at the output.
Already converting other document formats? markitdown handles PDF, Office and more, and its HTML output is competitive with the dedicated converters. One dependency rather than two is worth something.
Where a managed API fits
All four of these take HTML you already have. None fetches a page, renders JavaScript, or handles an anti-bot layer — and for a lot of real targets, that is the harder half.
Our own developer stack at Thunderbit covers that side. POST /distill takes a URL and returns clean LLM-ready Markdown with rendering and fetching handled; POST /extract returns AI schema-matched structured JSON against a JSON Schema you supply, which is a different output shape again — rows rather than a Markdown table you would then have to parse. Both are reachable from an MCP server and a CLI (npx @thunderbit/thunderbit-cli). Pricing is on the Thunderbit pricing page.
The honest comparison: if you hold the HTML and you want Markdown, markdownify is free and does the job well, and this table says which of its rivals do too. If you are fetching the pages, or you want structured rows rather than prose, that is a different purchase.
For the wider field, our web scraping API roundup covers hosted options and the open-source scraper pillar the self-hosted ones. Converting HTML to Markdown in Python is the practical walkthrough, and what llms.txt is trying to standardise covers where this whole category is heading.
Try Thunderbit for Web Data Extraction
Verdict
For this four-fixture workload, markdownify is the strongest default: the same emitted table-row count as markitdown under the shared counter, a token count within 1.3% of the other efficient converters, MIT, and a 1.8 MiB install.
That gap between popularity and measured behaviour is the finding. turndown is an excellent library that a great many people have installed without the plugin that makes it handle tables, and the cost of that shows up as a quarter more tokens and a table structure that no longer exists. Neither number appears anywhere until you count them.
If you take one thing: check what your converter does to a table before you trust its output to a model. Three of these four do something sensible. The most popular one does not, unless you asked it to.
Try Thunderbit for Web Data Extraction Get Started Free
FAQs
Does turndown really not support tables?
Its core does not. Tables come from turndown-plugin-gfm, a separate package, and the plain npm install turndown does not include it. Without the plugin, every cell survives as its own paragraph — the values are all there, the row and column relationships are not. Across four fixtures that produced zero Markdown table rows and 24.6% more tokens than markdownify for the same content.
Why did html2text look like it had no tables at first?
Because my counter required leading and trailing pipes and html2text does not emit them. Team Name | Year | Wins is legal Markdown and renders correctly; it is simply a different style from | Team Name | Year |. The corrected counter looks for a run of pipe-containing lines with a separator row, the way a parser does, and html2text goes from 1 row to 32. If you post-process Markdown with your own regex, this is the difference that will bite you.
Is the GPL on html2text a real problem?
It depends on how you combine and distribute the software. GPL-3.0-or-later can create obligations that MIT does not, so involve whoever owns licensing before selecting it for a distributed product. This is a compliance checkpoint, not legal advice; the licence identity was confirmed in registry metadata, the repository, and the installed package's METADATA.
Are these token counts meaningful for other pages?
The 24.6% gap is mostly turndown keeping <script> and <style> content, so it scales with how much of that a page carries — heavy on a modern CMS page, near zero on a static one. Tables move it the other way: on the fixture that is nothing but tables turndown came out 22% cheaper, because it emits no pipe scaffolding. Bytes per token sat between 3.61 and 3.65 for all four, so output density is the same everywhere and the difference is quantity. Measure your own corpus if the number matters to a budget.
What was not tested here?
Real-world variety — four fixtures is four fixtures. Nested lists, definition lists, footnotes, and math. Malformed HTML, which is where converters historically diverge most. Round-tripping the Markdown back to HTML. docling, which has the same fixtures on disk but whose published run does not report these fields, so it is absent rather than estimated. And configuration: html2text was run with body_width=0 because its default of 78 hard-wraps every line, which would have changed every character and token count in this table.


