console.log Is Slowly Killing Your Production Performance

40% CPU lost because of 20 forgotten console.log lines. console.log is synchronous & blocking in Node.js. 15 logs per request at 1000 RPS = 76% throughput drop.

· · 8 min read

I found this bug by accident. Production was slow - response time went from 50ms to 800ms during peak hours. CPU at 60%. Memory normal, database normal, queries fast, no I/O bottleneck. I was confused.

I profiled with clinic.js flame. In the flame graph, 40% of CPU time was spent on write - not file writes, not DB writes. Write to stdout. console.log.

I removed 20 lines of console.log that had accidentally been committed. Response time dropped to 50ms. CPU dropped from 60% to 25%.

This is an article you won't find on most dev blogs. Because it seems trivial - "ah, it's just console.log." But in production, the impact is real and significant. And almost no one writes about it.

---

console.log Is Synchronous & Blocking

Many assume console.log is async - writing to a stream without blocking code. It's not. In Node.js, console.log:

• Is synchronous when writing to process.stdout (default terminal output)
• Blocks the event loop until data is actually flushed

Why synchronous? So logs don't get lost on process crash. If console.log were async, the process could die before the log buffer is flushed. Node.js chooses safety over performance.

// This blocks - waits for data to actually be written to stdout
console.log('user logged in')

// This also blocks - console.error writes to stderr
console.error('something went wrong')

In development, 1-2 logs per request isn't noticeable. But in production, with thousands of requests per second and logging in every middleware + controller, it adds up.

I once saw: 1 request → 15 log lines. 1000 RPS → 15,000 log writes per second. Each write blocking for ~0.01ms. 15,000 × 0.01ms = 150ms extra per second just for logging.

---

Impact in Production: Real Numbers

I tested on a staging server (4 CPU, 8GB RAM) with a simple endpoint returning { ok: true }.

| Scenario | RPS | CPU | P99 Latency |
|---|---|---|---|
| No log | 5000 RPS | 22% | 8ms |
| 1 console.log per request | 4200 RPS | 35% | 12ms |
| 5 console.log per request | 2800 RPS | 55% | 22ms |
| 15 console.log per request | 1200 RPS | 68% | 48ms |

15 log lines per request: throughput drops 76%, latency increases 6x. Not because each log is heavy - but because console.log blocks the event loop, making other requests wait.

// Innocent-looking middleware that's costly at scale
app.use((req, res, next) => {
console.log(${req.method} ${req.path})
console.log('Headers:', req.headers)
console.log('Query:', req.query)
next()
})

app.get('/api/users/:id', async (req, res) => {
console.log('Fetching user:', req.params.id)
const user = await db.findUser(req.params.id)
console.log('User found:', user?.id)
console.log('User role:', user?.role)
console.log('Response time:', Date.now() - start)
res.json(user)
})

7 logs per request. In development: invisible. In production at 1000 RPS: 25-30% extra CPU usage.

---

The Worst Offender: Logging Request Bodies

// This one makes CPU explode
app.use((req, res, next) => {
console.log('Body:', JSON.stringify(req.body))
next()
})

JSON.stringify on a large request body + synchronous console.log = deadly combination. Especially when the body is a 1000-item array. Every request serializes unnecessarily, writes to stdout blocking.

Worse: you forget to remove it during development. The code makes it to production. Days go by without notice because there's no visible error.

---

How to Fix: Fire-and-Forget Logger

The solution isn't "don't log." Logging is still important. Just don't block.

Approach 1: Replace console.log with an async logger.

const pino = require('pino')
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
})

// Don't: console.log('user registered', userId)
// Do: logger.info({ userId }, 'user registered')

Pino (and Winston, Bunyan) can be configured for async writes - without blocking the event loop. Trade-off: if the process crashes before the buffer flushes, the last logs are lost.

Approach 2: Conditional logging - control with LOG_LEVEL.

if (process.env.LOG_LEVEL === 'debug') {
console.log('Processing order:', orderId)
}

Better: never commit verbose console.log. Use logger level filtering.

Approach 3: Batch logging - collect, flush periodically.

class BatchLogger {
constructor(intervalMs = 1000) {
this.buffer = []
setInterval(() => this.flush(), intervalMs)
}

log(level, message, data) {
this.buffer.push({ level, message, data, time: Date.now() })
}

flush() {
if (this.buffer.length === 0) return
const batch = this.buffer.splice(0)
process.stdout.write(JSON.stringify(batch) + '\n')
}
}

Batch logger: 1 syscall per second instead of 15,000 syscalls. Drastic improvement.

---

Debugging: How to Know If console.log Is Killing Your Performance

1. Stress test - compare RPS with and without logs
npx autocannon -c 100 -d 30 http://localhost:3000/api/test

2. Flame graph - look for 'write' in V8 internals
npx clinic flame -- node app.js

3. strace - check write syscall count
strace -c -p $(pgrep -f "node app") | head -20

If you see write consuming > 10% CPU in the flame graph, console.log is likely the culprit.

---

Common Misconceptions

"console.log is async." - No. It's sync and blocking, by design.

"A few log lines don't matter." - 1 line × 5000 RPS = 5000 blocking writes per second. Effects compound.

"Production uses PM2 with file output, so it's fine." - PM2 redirects stdout to a file. Redirecting is also blocking. The issue isn't "where" the log goes, but "how" it's written.

"Loggers like Winston are automatically fast." - Depends on config. Winston is also sync by default. You have to explicitly set async mode and accept the trade-off.

---

An Honest Closing

40% of my CPU was wasted on 20 console.log lines I forgot to remove. No error, no crash - just slowly degrading performance. And nobody writes about this because it seems "trivial."

The philosophy I learned: in production, blocking I/O is a silent killer. Not errors, not crashes - just performance slowly tanking without an obvious cause. And console.log is the most commonly ignored blocking I/O.

If you deploy code to production, check first: are there accidental console.log calls that made it into the commit? Verbose middleware logging? If so, remove them or switch to an async logger. Don't let 40% of your production CPU disappear just because you forgot to clean up debug logs.

---

Sources
• Node.js Console API
• OpenTelemetry Logs