Skip to content

@tus/server: avoid unnecessary POST_RECEIVE throttling - #858

Merged
Murderlon merged 3 commits into
tus:mainfrom
ferhatelmas:ferhat/throttle
Aug 11, 2026
Merged

@tus/server: avoid unnecessary POST_RECEIVE throttling#858
Murderlon merged 3 commits into
tus:mainfrom
ferhatelmas:ferhat/throttle

Conversation

@ferhatelmas

@ferhatelmas ferhatelmas commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
  • avoid its overhead when no listeners exist
  • cancel pending trailing timers when uploads settle

Open in Devin Review

@changeset-bot

changeset-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5ae3cae

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@tus/server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@Murderlon
Murderlon temporarily deployed to external-testing July 30, 2026 13:15 — with GitHub Actions Inactive

@Murderlon Murderlon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I handed this review over to Claude (Claude Code) for a deep code-quality pass. I've read it through and I'm posting it as my review.


Behavior goals are right, and the fake-timer tests are more deterministic than the wall-clock one in Server.test.ts:554. Also worth noting cancel() brings us into line with the README, which already documents that the trailing event isn't guaranteed. Two structural things I'd like to change first.

1. The optional throttle isn't earning its keep

BaseHandler.ts:187-203. The ternary saves one closure allocation per request, in a function that already allocates a PassThrough, a Readable, and a StreamLimiter. The cost you're actually removing is the per-chunk data listener — so gate only that:

const postReceive = throttle(/* unchanged */)

// Only pay per-chunk offset accounting when someone is listening.
if (this.listenerCount(EVENTS.POST_RECEIVE) > 0) {
  let tempOffset = upload.offset
  proxy.on('data', (chunk: Buffer) => {
    tempOffset += chunk.byteLength
    postReceive(tempOffset)
  })
}

Same perf win, and it drops the ternary, the | undefined, the if (postReceive) and the ?. — production diff shrinks to two lines.

2. writeToStore shouldn't be a manual Promise executor

The PR's whole point is cancel-before-settle, and it enforces that by ordering .finally() ahead of .then(resolve, reject). That's correct but unguarded — anyone "tidying" it back to .then().catch().finally() silently reverts the fix. And there are two settle paths: proxy.on('error') rejects outside the pipeline chain, so finally isn't guaranteed to have run. The window is microtask-scale so nothing leaks in practice, but the invariant is true by accident.

A try { return await stream.pipeline(...) } catch { /* AbortError mapping */ } finally { postReceive.cancel(); ... } gives one settle point and makes it a language guarantee. Bonus: the biome-ignore noAsyncPromiseExecutor on line 153 is already stale — there's no await in that executor. Happy for this to be a follow-up; if so, please comment that the .finally() ordering is load-bearing.

Smaller

  • Late-listener test. Fine to sample listenerCount once, but does not start progress tracking for a listener added during a write asserts an implementation artifact as a spec. Drop it, or document the constraint under POST_RECEIVE in the README.
  • createContext is the 7th hand-rolled copy (PatchHandler, PostHandler, HeadHandler, GetHandler, DeleteHandler, OptionsHandler). Please extract to src/test/utils.ts, and drop the {context} wrapper.
  • consumeStoreWrites is a 25-line bespoke waiter registry; a promise-per-chunk would read better.
  • Fake timers are installed suite-wide in before(), which also fakes mocha's own timeout timer. Won't collide at 100ms ticks, but a hang becomes a CI-job hang instead of a 30s failure. beforeEach/afterEach limits the blast radius.
  • Changeset should say the user-visible part plainly: consumers no longer get a final trailing POST_RECEIVE; use POST_FINISH for the terminal offset.

One correctness note in case it comes up: dropping the data listener does not affect backpressure — stream.pipeline still drives the proxy, and the pipe was already governing flow.

@ferhatelmas

Copy link
Copy Markdown
Contributor Author
  1. It's not the dominant cost but throttle allocates debounce state and nine nested functions. It's still good to prevent it in two lines.
  2. Addressed
  3. Addressed
  4. @tus/server: share cancellation context test helper #863
  5. Addressed
  6. Wrong for the version used
  7. Addressed

@ferhatelmas

Copy link
Copy Markdown
Contributor Author

@Murderlon shall we get this in?

@Murderlon Murderlon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor things, other than that LGTM

Comment thread packages/server/src/test/BaseHandler.test.ts Outdated
Comment thread packages/server/src/test/BaseHandler.test.ts Outdated
Murderlon pushed a commit that referenced this pull request Aug 10, 2026
Replace duplicated handler-test AbortController setup with a shared
createContext helper.

related to #858
<!-- devin-review-badge-begin -->

---

<a href="https://app.devin.ai/review/tus/tus-node-server/pull/863"
target="_blank">
  <picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1">
<img
src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1"
alt="Open in Devin Review">
  </picture>
</a>
<!-- devin-review-badge-end -->

Signed-off-by: ferhat elmas <elmas.ferhat@gmail.com>
* avoid its overhead when no listeners exist
* cancel pending trailing timers when uploads settle

Signed-off-by: ferhat elmas <elmas.ferhat@gmail.com>
Signed-off-by: ferhat elmas <elmas.ferhat@gmail.com>
Signed-off-by: ferhat elmas <elmas.ferhat@gmail.com>
@Murderlon
Murderlon added this pull request to the merge queue Aug 11, 2026
Merged via the queue into tus:main with commit 92db7b8 Aug 11, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants