Plate 66
logging vs print: Throughput Lab
Hands-on logging vs print throughput lab: real lines/s for StreamHandler, StringIO, and a disabled logger, measured on Linux localhost only (eng lab).
Aditya Challa4 min read
Intro — what this post promises
Is logging “just print with levels”? This lab counts lines/s for print vs logging.INFO through a StreamHandler to /dev/null and StringIO, plus a disabled logger and a level-filtered debug() call. The formatting story is measured, not assumed.
Related links:
- string concat vs join localhost lab
- set vs list membership localhost lab
- python re vs str methods localhost lab
- json vs orjson vs msgpack localhost lab
- base64 vs hex encode localhost lab
- pathlib vs os.path localhost lab
- sorted vs heapq vs bisect localhost lab
- Why your average latency graph is lying (p50 / p95 / p99)
Lab honesty (1 Oct 2026 IST): Python 3.13.5. 20,000 lines per batch. Formatter: %(levelname)s %(name)s %(message)s. Affiliates: 0. Not a journald/syslog/network handler study — local stream only.
Verdict up front: print to /dev/null ~5.14M lines/s. INFO logging to /dev/null ~159k (~0.031×, ~6282 ns/line). Pre-formatting the message only moves that to ~178k — the LogRecord + handler dominates, not % interpolation. A disabled logger stays near print (~5.09M).
Arms
| Arm | What happens |
|---|---|
| print StringIO / /dev/null | print(preformatted, file=...) |
log lazy % | log.info("...%s...", *args) — format at emit |
| log preformatted | log.info(already_built_string) |
| disabled | logger.disabled = True then info(...) |
| filtered debug | logger level INFO, call debug(...) |
| isEnabledFor then info | guard that still emits |
Lab topology
Script: lab-evidence/51-logging-vs-print/results/run_lab.py.
Lead table — lines/s (p50)
| Arm | lines/s | ns/line | vs print /dev/null |
|---|---|---|---|
| print StringIO | 5,838,163 | 171 | 1.14× |
| print /dev/null | 5,140,281 | 195 | 1.00× |
| logger disabled | 5,087,336 | 197 | 0.99× |
| debug() filtered at INFO | 4,588,180 | 218 | 0.89× |
| log /dev/null preformatted | 178,343 | 5607 | 0.035× |
| log /dev/null lazy % | 159,195 | 6282 | 0.031× |
| log StringIO preformatted | 170,463 | 5866 | 0.033× |
| log StringIO lazy % | 146,062 | 6846 | 0.028× |
| isEnabledFor + info /dev/null | 158,992 | 6290 | 0.031× |
Formatting is not the cliff
Lazy % vs preformatted on /dev/null: 159,195 vs 178,343 lines/s — about 1.12×, roughly 674 ns saved. The remaining ~5607 ns is record construction, formatter, and the handler lock/write even when the sink is /dev/null.
So: do pass args to log.info(fmt, *args) so a disabled/filtered call can skip interpolation — that path is cheap (filtered debug ~4.59M/s). Do not expect pre-building the string to make an enabled INFO logger competitive with print.
isEnabledFor then info on an enabled logger matches the slow path (~159k/s). The guard only helps when the level is off.
StringIO vs /dev/null is a small gap next to the logging tax (print StringIO even slightly faster here — buffer, no device).
Pitfalls
- Benchmarking only
logger.disabledand concluding logging is free — the hot path is an enabled handler. - Blaming f-strings inside
info(f"...")— you pay interpolation even when filtered. Use%args orisEnabledFor. - Root logger + propagate — extra handlers multiply this cost. This lab sets
propagate = False. - /dev/null is not production — a file, queue, or network handler is slower still.
When to pick what
| Need | Prefer |
|---|---|
| Debug noise in a tight loop | level filter or disable; % args not f-strings |
| A few operational lines per request | logging — clarity beats the microseconds |
| Millions of lines/s | not the logging module; sample or counters |
| “print is faster so ship print” | only if you do not need levels — measured ~32× here |
Reproduce
Evidence: /workspace/lab-evidence/51-logging-vs-print/results/.
Closing
Enabled logging is a different cost class than print. On this box print to /dev/null is ~5.14M lines/s; INFO StreamHandler is ~159k (~32× slower). Preformatting saves only ~674 ns. A disabled or filtered logger stays near ~5.1–4.6M/s. Log the lines you mean; don’t log inside the inner loop unless you measured the level as off.
Lab evidence
What I found running this
Lab 1 Oct 2026 IST. Python 3.13.5; N=20000 lines. print /dev/null 5.14M lines/s; print StringIO 5.84M. log StreamHandler /dev/null lazy % 159k (~0.031x print); preformatted 178k. disabled logger 5.09M; debug filtered at INFO 4.59M. Affiliates: 0. Evidence: lab-evidence/51-logging-vs-print/.
Related links
Plate 22
string concat vs join vs StringIO: Build Lab
A hands-on localhost lab comparing +=, list+join, preallocated join, and StringIO for building strings at real sizes.
Observability & SRE · 30 Sept 2026
Plate 47
logging.Formatter vs f-string: Localhost Lab
1 Oct 2026
Plate 17
platform vs os.uname Inventory: Localhost Lab
Hands-on platform.platform vs os.uname host inventory lab: real ops/s plus cache notes, measured on Linux localhost today in this hands-on lab for SREs.
1 Oct 2026