ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
About

Plate 66

  1. Blog

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 Challa·30 September 2026·4 min read

Summary
On this page
  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table — lines/s (p50)
  5. Formatting is not the cliff
  6. Pitfalls
  7. When to pick what
  8. Reproduce
  9. Closing

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

ArmWhat happens
print StringIO / /dev/nullprint(preformatted, file=...)
log lazy %log.info("...%s...", *args) — format at emit
log preformattedlog.info(already_built_string)
disabledlogger.disabled = True then info(...)
filtered debuglogger level INFO, call debug(...)
isEnabledFor then infoguard that still emits

Lab topology

N = 20000 lines / repeat, 7 repeats, p50
Message: request id / path / status / elapsed
Sinks: /dev/null and io.StringIO
No propagation; one StreamHandler

Script: lab-evidence/51-logging-vs-print/results/run_lab.py.


Lead table — lines/s (p50)

Armlines/sns/linevs print /dev/null
print StringIO5,838,1631711.14×
print /dev/null5,140,2811951.00×
logger disabled5,087,3361970.99×
debug() filtered at INFO4,588,1802180.89×
log /dev/null preformatted178,34356070.035×
log /dev/null lazy %159,19562820.031×
log StringIO preformatted170,46358660.033×
log StringIO lazy %146,06268460.028×
isEnabledFor + info /dev/null158,99262900.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

  1. Benchmarking only logger.disabled and concluding logging is free — the hot path is an enabled handler.
  2. Blaming f-strings inside info(f"...") — you pay interpolation even when filtered. Use % args or isEnabledFor.
  3. Root logger + propagate — extra handlers multiply this cost. This lab sets propagate = False.
  4. /dev/null is not production — a file, queue, or network handler is slower still.

When to pick what

NeedPrefer
Debug noise in a tight looplevel filter or disable; % args not f-strings
A few operational lines per requestlogging — clarity beats the microseconds
Millions of lines/snot the logging module; sample or counters
“print is faster so ship print”only if you do not need levels — measured ~32× here

Reproduce

python3 lab-evidence/51-logging-vs-print/results/run_lab.py

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.

logging vs printstreamhandlerdisabled loggerpython loggingstringiolocalhost labsrethroughput

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/.

Notes when a lab post goes up

Occasional email for new hands-on reviews. No sequence and no sponsors.

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

On this page

  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table — lines/s (p50)
  5. Formatting is not the cliff
  6. Pitfalls
  7. When to pick what
  8. Reproduce
  9. Closing
All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove