Conversation
_processMessage built `msg.content.toString()` up front and kept it as a local for the debug log and the error log. V8 keeps every local of a paused async function alive, so each in-flight message held a full string copy of its payload for as long as the handler ran. With large messages and slow handlers, this copy grows with prefetch and can dominate the heap. - Drop the debug log of the full payload. The logger contract has no way to ask whether debug is enabled, so it cost a copy every time. - The error log builds the payload string inside the catch block and cuts it to the first 8192 bytes.
yosiat
commented
Oct 3, 2026
| async _processMessage(channel, subscription, msg) { | ||
| const { queue, callback } = subscription; | ||
| const messageString = msg.content.toString(); | ||
| logger.debug({ |
Contributor
Author
There was a problem hiding this comment.
I can add support to logger interface to do check for debug enabled, but in reality we never look at debug logs here.
If you think it's crucial I'll do so.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The core memory and truncation behavior lacks automated regression coverage.
Review effort: Balanced
Findings: 1
What changed in this PR
Reduces memory retained while consumer handlers run by deferring and limiting payload stringification.
Changes:
- Removes full-payload debug logging.
- Truncates error-log payloads to 8192 bytes.
- Bumps the package version to 0.18.1.
| File | Description |
|---|---|
src/modules/consumer.js |
Avoids persistent payload string copies during processing. |
package.json |
Bumps the patch version. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| params: { queue, message: messageString }, | ||
| // Built here and cut, never kept as a local: V8 keeps every local of this async function | ||
| // alive for as long as the handler is awaited. | ||
| params: { queue, message: msg.content.toString('utf8', 0, ERROR_LOG_PAYLOAD_BYTES) }, |
ramhr
approved these changes
Oct 4, 2026
shamil
approved these changes
Oct 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
_processMessageconverted every delivery to a string up front:The string was used by a debug log before the handler and by the error log in
the
catchblock. Because it is a local of an async function, V8 keeps italive for as long as the function is paused at
await action.callback(...).So every in-flight message held a full string copy of its payload for the
whole run of its handler, on top of
msg.contentitself. A payload with anynon-Latin-1 character is stored as a two-byte string, which doubles the copy.
With large messages, a high prefetch and slow handlers, these copies add up
to
prefetch x payload sizeof heap that nothing uses.Changes
to check whether debug is enabled, so the copy was paid on every message.
catchblock andcuts it to the first 8192 bytes:
msg.content.toString('utf8', 0, 8192).No public API change. The error log keeps the queue name and the error; only
params.messageis cut.How it was checked
A standalone script drives the real
Consumerwith a fake channel (nobroker). It subscribes a handler that never finishes and delivers 200
messages of 1 MB each. The payload is JSON padded with whitespace, so the
raw message is large while the parsed body is tiny: any heap growth comes
from a string copy of the payload. Heap measured after GC, Node 22:
Script