Skip to content

fix(webapp): eliminate SSE abort-signal memory leak#3430

Merged
myftija merged 5 commits intomainfrom
fix/webapp-sse-memory-leak
Apr 23, 2026
Merged

fix(webapp): eliminate SSE abort-signal memory leak#3430
myftija merged 5 commits intomainfrom
fix/webapp-sse-memory-leak

Conversation

@ericallam
Copy link
Copy Markdown
Member

@ericallam ericallam commented Apr 23, 2026

Summary

Fixes a server-side memory leak in the webapp's SSE helper. Every aborted SSE connection (client tab close, navigation, timeout) was pinning its full request/response graph indefinitely on Node 20, so any long-running webapp process accumulated retained memory proportional to streaming-request churn.

Root cause

apps/webapp/app/utils/sse.ts combined four abort signals via AbortSignal.any([requestAbortSignal, timeoutSignal, internalController.signal]). The composite signal tracks its source signals in an internal Set<WeakRef> registered against a FinalizationRegistry; under sustained traffic those entries accumulate faster than they're cleaned up, pinning every source signal (and its listeners, and anything those listeners close over) until the parent signal itself is GC'd or aborts.

This is a long-standing Node issue with multiple open reports:

  • nodejs/node#54614 — original report, still open. A follow-up from ChainSafe describes the exact same shape in a Lodestar production workload (req + timeout signals composed per request accumulating in long-running worker) and the same mitigation: drop AbortSignal.any, compose manually.
  • nodejs/node#55351 — mechanism confirmed by Node member @jasnell: "the set of dependent signals known to the AbortSignal are kept in an internal Set using WeakRefs. The AbortSignals are being properly gc'd but the Set is never cleaned out of the WeakRefs making those leak." Partially fixed by PR #55354, shipped in Node 22.12.0 — but only covers the tight-loop case, not long-lived parent signals.
  • nodejs/node#57584 — circular-dependency variant, still open.
  • nodejs/node#62363 — regression in Node 24/25 from an unrelated V8 change ("Don't pretenure WeakCells"). Different root cause, same symptom.

A separate issue in apps/webapp/app/entry.server.tsxsetTimeout(abort, ABORT_DELAY) with no clearTimeout on success paths — kept the React render tree + remixContext alive for 30s per successful HTML request. Same pattern fixed upstream in React Router templates (react-router#14200), never backported to Remix v2.

What changed

  • apps/webapp/app/utils/sse.ts — single-signal abort chain. AbortSignal.any removed; AbortSignal.timeout replaced by a plain setTimeout cleared when the controller aborts; named sentinel constants used as stackless abort reasons; request-abort handler explicitly removed on cleanup.
  • apps/webapp/app/entry.server.tsx — clears the setTimeout(abort, ABORT_DELAY) timer in onShellReady / onAllReady / onShellError.
  • apps/webapp/app/v3/tracer.server.ts + env.server.ts — gates OpenTelemetry HttpInstrumentation and ExpressInstrumentation behind DISABLE_HTTP_INSTRUMENTATION=true as an escape hatch for future OTel-listener retention patterns. Defaults to enabled.
  • apps/webapp/app/presenters/v3/RunStreamPresenter.server.ts — uses the shared ABORT_REASON_SEND_ERROR sentinel.

Verification

Full-app reproduction (memlab)

Isolated local harness, 500 abrupt SSE disconnects against a dev-presence route, GC between passes, heap snapshot diff with memlab:

Run Heap delta after 500 conns + GC memlab retained leaks
Before +16.0 MB (linear with request count) 158 clusters; 250 ServerResponse, 1000 AbortController, 250 SpanImpl retained
After +3.3 MB (noise) 0 app-code leaks

Standalone mechanism isolation

To confirm which axis of the change is load-bearing, a separate standalone Node script (/tmp/abort-leak-test.mjs) ran 2000 requests × 200 KB payload per variant:

Variant Heap delta after GC
baseline (no signal machinery) 0 MB
V1: AbortSignal.any + string abort reason +9.1 MB
V2: AbortSignal.any only (no reason) +10.8 MB
V3: string reason only (no AbortSignal.any) 0 MB
V4: neither (the fix) 0 MB
V5: AbortSignal.any with no listener on the composite +10.2 MB

This proves AbortSignal.any is the sole mechanism. The reason type (.abort() vs .abort("string")) is irrelevant for retention — V3 is clean, V5 leaks even without a listener on the composite.

Risk

  • sse.ts is used by the dev-presence routes. Behaviour is equivalent — timeouts and client disconnects still abort the stream. signal.reason is now a named string sentinel ("timeout", "request_aborted", etc.) instead of the previous string arg or default AbortError. No in-tree reader of signal.reason exists.
  • entry.server.tsx change is a standard cleanup of an abort timer, matches upstream React Router guidance.
  • tracer.server.ts change is env-gated and defaults to current behaviour.
  • Three other webapp AbortSignal.timeout() callsites (alert delivery, remote-build status) are fire-and-forget passed directly to fetch — not composed with anything long-lived, no retention risk, untouched.

Test plan

  • Existing SSE integration tests pass
  • Dev-presence SSE behaves normally across tab open/close cycles
  • No heap growth under sustained aborted-connection traffic (heap snapshot diff)

Follow-up

The same AbortSignal.any([userSignal, internalSignal]) pattern exists in several SDK/core callsites that ship to customers (packages/core/src/v3/realtimeStreams/manager.ts, packages/trigger-sdk/src/v3/{ai,chat,chat-client,sessions}.ts, packages/core/src/v3/workers/warmStartClient.ts). Whether those leak in practice depends on the user passing a long-lived signal. Tracked separately.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 23, 2026

⚠️ No Changeset found

Latest commit: 6ae1a1f

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a server changelog entry and fixes an SSE memory leak by reworking abort/cancellation wiring: removes AbortSignal.any() and string abort reasons, introduces an internalController used directly by event streams, makes the timeout-driven abort clearable and synchronously invokable on request abort, and ensures internal aborts occur without string reasons. entry.server.tsx now stores and clears the abort timer on terminal paths. RunStreamPresenter aborts without a reason when send throws. Telemetry initialization now conditionally prepends HTTP/Express instrumentation based on a new DISABLE_HTTP_INSTRUMENTATION env flag added to the environment schema.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(webapp): eliminate SSE abort-signal memory leak' directly describes the primary change—eliminating a memory leak in SSE abort signal handling—and uses clear, specific language.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering root cause analysis, changes made, verification results, risk assessment, and test plan—exceeding template requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/webapp-sse-memory-leak

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

devin-ai-integration[bot]

This comment was marked as resolved.

AbortSignal.any() combined with abort(reason) on Node 20 pinned the full
Express request/response graph via a DOMException stack trace and a
FinalizationRegistry. Every aborted SSE connection leaked ~60KB
(ServerResponse, IncomingMessage, Socket, 4x AbortController, SpanImpl)
that survived GC indefinitely.

- sse.ts: collapse the 4-signal stack to a single internalController.signal;
  replace AbortSignal.timeout() with a plain setTimeout cleared on abort;
  drop every string arg from .abort() so no DOMException stack is captured;
  removeEventListener the request-abort handler on cleanup.
- entry.server.tsx: clear the setTimeout(abort, ABORT_DELAY) in every
  terminal callback so the abort closure does not pin the React render
  tree + remixContext for 30s per successful request (react-router #14200).
- tracer.server.ts: gate HttpInstrumentation and ExpressInstrumentation
  behind DISABLE_HTTP_INSTRUMENTATION=true for isolation testing; defaults
  to enabled.

Verified locally (500 aborted SSE connections + GC):
- unpatched: +16.0 MB heap, 158 memlab leak clusters
- patched:   +3.3 MB heap, 0 app-code leaks
coderabbitai[bot]

This comment was marked as resolved.

@ericallam ericallam force-pushed the fix/webapp-sse-memory-leak branch from 4e9d0c5 to 5a9563a Compare April 23, 2026 10:56
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
apps/webapp/app/v3/tracer.server.ts (1)

310-312: ⚠️ Potential issue | 🟡 Minor

Route DISABLE_HTTP_INSTRUMENTATION through env.server.ts.

Line 310 still bypasses the webapp env schema. Add DISABLE_HTTP_INSTRUMENTATION to the env export, then use env.DISABLE_HTTP_INSTRUMENTATION here.

Suggested local change
-  if (process.env.DISABLE_HTTP_INSTRUMENTATION !== "true") {
+  if (env.DISABLE_HTTP_INSTRUMENTATION !== "true") {
     instrumentations.unshift(new HttpInstrumentation(), new ExpressInstrumentation());
   }

As per coding guidelines, “Access environment variables via env export from app/env.server.ts. Never use process.env directly”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/v3/tracer.server.ts` around lines 310 - 312, Add
DISABLE_HTTP_INSTRUMENTATION to the env export in app/env.server.ts (the same
env object used throughout the app) and then replace the direct process.env
access in tracer.server.ts with env.DISABLE_HTTP_INSTRUMENTATION; update any
associated types or schema in env.server.ts (e.g., the exported Env interface or
zod/schema validator) so the new key is typed/validated, and change the
conditional in instrumentations.unshift(...) to check
env.DISABLE_HTTP_INSTRUMENTATION === "true".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/webapp/app/v3/tracer.server.ts`:
- Around line 310-312: Add DISABLE_HTTP_INSTRUMENTATION to the env export in
app/env.server.ts (the same env object used throughout the app) and then replace
the direct process.env access in tracer.server.ts with
env.DISABLE_HTTP_INSTRUMENTATION; update any associated types or schema in
env.server.ts (e.g., the exported Env interface or zod/schema validator) so the
new key is typed/validated, and change the conditional in
instrumentations.unshift(...) to check env.DISABLE_HTTP_INSTRUMENTATION ===
"true".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b2a38674-b80b-4e0a-876b-26c8e6f88736

📥 Commits

Reviewing files that changed from the base of the PR and between 4e9d0c5 and 5a9563a.

📒 Files selected for processing (4)
  • .server-changes/fix-sse-memory-leak.md
  • apps/webapp/app/entry.server.tsx
  • apps/webapp/app/utils/sse.ts
  • apps/webapp/app/v3/tracer.server.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/webapp/app/entry.server.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • .server-changes/fix-sse-memory-leak.md
  • apps/webapp/app/utils/sse.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (28)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: sdk-compat / Node.js 20.20 (ubuntu-latest)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: sdk-compat / Node.js 22.12 (ubuntu-latest)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: sdk-compat / Cloudflare Workers
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: sdk-compat / Bun Runtime
  • GitHub Check: sdk-compat / Deno Runtime
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

Files:

  • apps/webapp/app/v3/tracer.server.ts
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/app/v3/tracer.server.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Add crumbs as you write code using // @Crumbs comments or `// `#region` `@crumbs blocks. These are temporary debug instrumentation and must be stripped using agentcrumbs strip before merge.

Files:

  • apps/webapp/app/v3/tracer.server.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries

Files:

  • apps/webapp/app/v3/tracer.server.ts
**/*.{js,ts,jsx,tsx,json,md,yaml,yml}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier before committing

Files:

  • apps/webapp/app/v3/tracer.server.ts
**/*.ts{,x}

📄 CodeRabbit inference engine (CLAUDE.md)

Always import from @trigger.dev/sdk when writing Trigger.dev tasks. Never use @trigger.dev/sdk/v3 or deprecated client.defineJob.

Files:

  • apps/webapp/app/v3/tracer.server.ts
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: Access environment variables through the env export of env.server.ts instead of directly accessing process.env
Use subpath exports from @trigger.dev/core package instead of importing from the root @trigger.dev/core path

Use named constants for sentinel/placeholder values (e.g. const UNSET_VALUE = '__unset__') instead of raw string literals scattered across comparisons

Files:

  • apps/webapp/app/v3/tracer.server.ts
apps/webapp/**/*.server.ts

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

apps/webapp/**/*.server.ts: Never use request.signal for detecting client disconnects. Use getRequestAbortSignal() from app/services/httpAsyncStorage.server.ts instead, which is wired directly to Express res.on('close') and fires reliably
Access environment variables via env export from app/env.server.ts. Never use process.env directly
Always use findFirst instead of findUnique in Prisma queries. findUnique has an implicit DataLoader that batches concurrent calls and has active bugs even in Prisma 6.x (uppercase UUIDs returning null, composite key SQL correctness issues, 5-10x worse performance). findFirst is never batched and avoids this entire class of issues

Files:

  • apps/webapp/app/v3/tracer.server.ts
🧠 Learnings (19)
📓 Common learnings
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3399
File: apps/webapp/app/services/realtime/redisRealtimeStreams.server.ts:282-291
Timestamp: 2026-04-16T14:07:46.808Z
Learning: In `apps/webapp/app/services/realtime/redisRealtimeStreams.server.ts` (`streamResponse`), the pattern `signal.addEventListener("abort", cleanup, { once: true })` does NOT need an explicit `removeEventListener` call in the non-abort cleanup paths (inactivity, cancel). The `AbortController` is per-request, scoped to `httpAsyncStorage` (created in `apps/webapp/server.ts` per-request middleware), so it gets GC'd when the request ends — taking the listener and closure with it. The `isCleanedUp` guard prevents double-execution, and `redis.disconnect()` is called before the request ends. Do not flag this as a listener/closure leak.
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: apps/webapp/CLAUDE.md:0-0
Timestamp: 2026-04-16T14:19:16.330Z
Learning: Applies to apps/webapp/**/*.server.ts : Never use `request.signal` for detecting client disconnects. Use `getRequestAbortSignal()` from `app/services/httpAsyncStorage.server.ts` instead, which is wired directly to Express `res.on('close')` and fires reliably
📚 Learning: 2026-03-25T15:29:25.889Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2026-03-25T15:29:25.889Z
Learning: Applies to trigger.config.ts : Configure telemetry instrumentations and exporters in the `telemetry` option

Applied to files:

  • apps/webapp/app/v3/tracer.server.ts
📚 Learning: 2026-03-02T12:43:25.254Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: internal-packages/run-engine/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:25.254Z
Learning: Applies to internal-packages/run-engine/src/engine/systems/**/*.ts : Integrate OpenTelemetry tracer and meter instrumentation in RunEngine systems for observability

Applied to files:

  • apps/webapp/app/v3/tracer.server.ts
📚 Learning: 2026-04-13T21:44:00.032Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3368
File: apps/webapp/app/services/taskIdentifierRegistry.server.ts:24-67
Timestamp: 2026-04-13T21:44:00.032Z
Learning: In `apps/webapp/app/services/taskIdentifierRegistry.server.ts`, the sequential upsert/updateMany/findMany writes in `syncTaskIdentifiers` are intentionally NOT wrapped in a Prisma transaction. This function runs only during deployment-change events (low-concurrency path), and any partial `isInLatestDeployment` state is acceptable because it self-corrects on the next deployment. Do not flag this as a missing-transaction/atomicity issue in future reviews.

Applied to files:

  • apps/webapp/app/v3/tracer.server.ts
📚 Learning: 2026-04-15T15:39:31.575Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2026-04-15T15:39:31.575Z
Learning: Applies to apps/webapp/**/*.{ts,tsx} : Access environment variables through the `env` export of `env.server.ts` instead of directly accessing `process.env`

Applied to files:

  • apps/webapp/app/v3/tracer.server.ts
📚 Learning: 2026-04-16T14:19:16.330Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: apps/webapp/CLAUDE.md:0-0
Timestamp: 2026-04-16T14:19:16.330Z
Learning: Applies to apps/webapp/**/*.server.ts : Access environment variables via `env` export from `app/env.server.ts`. Never use `process.env` directly

Applied to files:

  • apps/webapp/app/v3/tracer.server.ts
📚 Learning: 2026-04-15T15:39:31.575Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2026-04-15T15:39:31.575Z
Learning: Applies to apps/webapp/**/*.test.{ts,tsx} : Do not import `env.server.ts` directly or indirectly into test files; instead pass environment-dependent values through options/parameters to make code testable

Applied to files:

  • apps/webapp/app/v3/tracer.server.ts
📚 Learning: 2025-08-14T18:35:44.370Z
Learnt from: nicktrn
Repo: triggerdotdev/trigger.dev PR: 2390
File: apps/webapp/app/env.server.ts:764-765
Timestamp: 2025-08-14T18:35:44.370Z
Learning: The BoolEnv helper in apps/webapp/app/utils/boolEnv.ts uses z.preprocess with inconsistent default value types across the codebase - some usages pass boolean defaults (correct) while others pass string defaults (incorrect), leading to type confusion. The helper should enforce boolean-only defaults or have clearer documentation.

Applied to files:

  • apps/webapp/app/v3/tracer.server.ts
📚 Learning: 2026-03-27T18:11:57.032Z
Learnt from: nicktrn
Repo: triggerdotdev/trigger.dev PR: 3114
File: apps/supervisor/src/index.ts:83-98
Timestamp: 2026-03-27T18:11:57.032Z
Learning: In `apps/supervisor/src/index.ts`, `RESOURCE_MONITOR_ENABLED` (env var in `apps/supervisor/src/env.ts`) defaults to `false`. As a result, the local `ResourceMonitor`-based `maxResources`/`skipDequeue` gating in `preDequeue` is inactive in compute mode deployments. Do not flag local resource monitor usage in compute mode as a live bug; it has no practical impact unless `RESOURCE_MONITOR_ENABLED` is explicitly set to `true`.

Applied to files:

  • apps/webapp/app/v3/tracer.server.ts
📚 Learning: 2025-08-19T09:49:07.011Z
Learnt from: julienvanbeveren
Repo: triggerdotdev/trigger.dev PR: 2417
File: apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.$slug.import.ts:56-61
Timestamp: 2025-08-19T09:49:07.011Z
Learning: In the Trigger.dev codebase, environment variables should default to `isSecret: false` when not explicitly marked as secrets in the syncEnvVars functionality. This is the intended behavior for both regular variables and parent variables.

Applied to files:

  • apps/webapp/app/v3/tracer.server.ts
📚 Learning: 2026-04-16T14:19:16.330Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: apps/webapp/CLAUDE.md:0-0
Timestamp: 2026-04-16T14:19:16.330Z
Learning: Applies to apps/webapp/**/*.test.{ts,tsx} : For testable code, never import `env.server.ts` in test files. Pass configuration as options instead (e.g., `realtimeClient.server.ts` takes config as constructor arg, `realtimeClientGlobal.server.ts` creates singleton with env config)

Applied to files:

  • apps/webapp/app/v3/tracer.server.ts
📚 Learning: 2026-04-16T13:45:22.317Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3368
File: apps/webapp/test/engine/taskIdentifierRegistry.test.ts:3-19
Timestamp: 2026-04-16T13:45:22.317Z
Learning: In `apps/webapp/test/engine/taskIdentifierRegistry.test.ts`, the `vi.mock` calls for `~/services/taskIdentifierCache.server` (stubbing `getTaskIdentifiersFromCache` and `populateTaskIdentifierCache`), `~/models/task.server` (stubbing `getAllTaskIdentifiers`), and `~/db.server` (stubbing `prisma` and `$replica`) are intentional. The suite uses real Postgres via testcontainers for all `TaskIdentifier` DB operations, but isolates the Redis cache layer and legacy query fallback as separate concerns not exercised in this test file. Do not flag these mocks as violations of the no-mocks policy in future reviews.

Applied to files:

  • apps/webapp/app/v3/tracer.server.ts
📚 Learning: 2026-03-02T12:42:47.652Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: apps/supervisor/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:42:47.652Z
Learning: Applies to apps/supervisor/src/env.ts : Environment configuration should be defined in `src/env.ts`

Applied to files:

  • apps/webapp/app/v3/tracer.server.ts
📚 Learning: 2025-11-10T09:09:07.399Z
Learnt from: myftija
Repo: triggerdotdev/trigger.dev PR: 2663
File: apps/webapp/app/env.server.ts:1205-1206
Timestamp: 2025-11-10T09:09:07.399Z
Learning: In the trigger.dev webapp, S2_ACCESS_TOKEN and S2_DEPLOYMENT_LOGS_BASIN_NAME environment variables must remain optional until an OSS version of S2 is available, to avoid breaking environments that don't have S2 provisioned.

Applied to files:

  • apps/webapp/app/v3/tracer.server.ts
📚 Learning: 2026-04-07T14:12:18.946Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3331
File: apps/webapp/test/engine/batchPayloads.test.ts:5-24
Timestamp: 2026-04-07T14:12:18.946Z
Learning: In `apps/webapp/test/engine/batchPayloads.test.ts`, using `vi.mock` for `~/v3/objectStore.server` (stubbing `hasObjectStoreClient` and `uploadPacketToObjectStore`), `~/env.server` (overriding offload thresholds), and `~/v3/tracer.server` (stubbing `startActiveSpan`) is intentional and acceptable. Simulating controlled transient upload failures (e.g., fail N times then succeed) to verify `p-retry` behavior cannot be reproduced with real services or testcontainers. This file is an explicit exception to the repo's general no-mocks policy.

Applied to files:

  • apps/webapp/app/v3/tracer.server.ts
📚 Learning: 2026-04-16T14:19:16.330Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: apps/webapp/CLAUDE.md:0-0
Timestamp: 2026-04-16T14:19:16.330Z
Learning: Applies to apps/webapp/**/*.{ts,tsx} : Use named constants for sentinel/placeholder values (e.g. `const UNSET_VALUE = '__unset__'`) instead of raw string literals scattered across comparisons

Applied to files:

  • apps/webapp/app/v3/tracer.server.ts
📚 Learning: 2026-03-22T13:26:12.060Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3244
File: apps/webapp/app/components/code/TextEditor.tsx:81-86
Timestamp: 2026-03-22T13:26:12.060Z
Learning: In the triggerdotdev/trigger.dev codebase, do not flag `navigator.clipboard.writeText(...)` calls for `missing-await`/`unhandled-promise` issues. These clipboard writes are intentionally invoked without `await` and without `catch` handlers across the project; keep that behavior consistent when reviewing TypeScript/TSX files (e.g., usages like in `apps/webapp/app/components/code/TextEditor.tsx`).

Applied to files:

  • apps/webapp/app/v3/tracer.server.ts
📚 Learning: 2026-03-22T19:24:14.403Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3187
File: apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts:200-204
Timestamp: 2026-03-22T19:24:14.403Z
Learning: In the triggerdotdev/trigger.dev codebase, webhook URLs are not expected to contain embedded credentials/secrets (e.g., fields like `ProjectAlertWebhookProperties` should only hold credential-free webhook endpoints). During code review, if you see logging or inclusion of raw webhook URLs in error messages, do not automatically treat it as a credential-leak/secrets-in-logs issue by default—first verify the URL does not contain embedded credentials (for example, no username/password in the URL, no obvious secret/token query params or fragments). If the URL is credential-free per this project’s conventions, allow the logging.

Applied to files:

  • apps/webapp/app/v3/tracer.server.ts
📚 Learning: 2026-03-29T19:16:28.864Z
Learnt from: nicktrn
Repo: triggerdotdev/trigger.dev PR: 3291
File: apps/webapp/app/v3/featureFlags.ts:53-65
Timestamp: 2026-03-29T19:16:28.864Z
Learning: When reviewing TypeScript code that uses Zod v3, treat `z.coerce.*()` schemas as their direct Zod type (e.g., `z.coerce.boolean()` returns a `ZodBoolean` with `_def.typeName === "ZodBoolean"`) rather than a `ZodEffects`. Only `.preprocess()`, `.refine()`/`.superRefine()`, and `.transform()` are expected to wrap schemas in `ZodEffects`. Therefore, in reviewers’ logic like `getFlagControlType`, do not flag/unblock failures that require unwrapping `ZodEffects` when the input schema is a `z.coerce.*` schema.

Applied to files:

  • apps/webapp/app/v3/tracer.server.ts

devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

Switch DISABLE_HTTP_INSTRUMENTATION from z.string().default("false") to
BoolEnv.default(false) for consistency with other boolean env flags, and
update the tracer check to use the parsed boolean directly.
devin-ai-integration[bot]

This comment was marked as resolved.

The same internalController is exposed to handlers via context.controller.
RunStreamPresenter was still calling .abort("Send error"), which creates
a DOMException with a stack trace on signal.reason. Without AbortSignal.any
this no longer pins the closure graph, but it's inconsistent with the rest
of the fix and worth cleaning up.
coderabbitai[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

After verifying the mechanism with a standalone isolation test (see PR),
AbortSignal.any's dependent-signal tracking is the sole cause of the leak;
the reason type (.abort() vs .abort("string")) is not. Keeping the signal
chain collapsed to a single AbortController is what eliminates retention.

- Add named sentinel constants (ABORT_REASON_*) for readability and to
  satisfy the CLAUDE.md named-constant rule.
- Route RunStreamPresenter's send-error abort through the shared
  ABORT_REASON_SEND_ERROR sentinel.
- Update the sse.ts comment to cite nodejs/node#54614 (production shape
  reported by ChainSafe Lodestar in the same retention pattern) and
  nodejs/node#55351 (mechanism confirmed by @jasnell, narrow fix
  shipped in 22.12.0 via #55354).
- Correct the .server-changes entry: the leak is caused by AbortSignal.any,
  not by the string abort reasons (the earlier phrasing conflated the
  two).
@myftija myftija merged commit 486f497 into main Apr 23, 2026
45 checks passed
@myftija myftija deleted the fix/webapp-sse-memory-leak branch April 23, 2026 13:54
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