Postboy Help

How-to: Safely Manage Middleware Resources (dispose, leaks, and lifecycle ownership)

Problem

Your middleware owns resources (subscriptions, timers, caches, external handles). You need to:

  • avoid memory leaks,

  • avoid cross-test or cross-module contamination,

  • ensure predictable cleanup,

  • keep middleware safe even when operations are cancelled (Interrupt) or fail.

Prerequisites

  • You understand the middleware lifecycle hooks:

    • canHandle (filter)

    • before/after (per-operation hooks)

    • dispose (cleanup)

  • You know when middleware is attached and removed in your app lifecycle.

  • You know that Interrupt/cancel happens in before and can prevent after from being called.

Steps

1) Identify what your middleware “owns”

Make an explicit list of resources your middleware creates. Typical categories:

Long-lived (must be released in dispose)

  • RxJS subscriptions created by middleware itself

  • intervals/timeouts that repeat

  • event listeners (process, window, DOM, custom emitters)

  • external clients/handles created inside middleware

Per-operation temporary state (must not leak if after is skipped)

  • start timestamps for timing

  • correlation maps keyed by message id

  • short-lived buffers or counters

2) Decide who owns middleware lifetime

Pick one ownership model and stick to it:

  1. App-owned (global)

  • middleware is attached once at startup

  • it lives as long as the bus lives

  • cleanup happens when the bus is disposed / app shuts down

  1. Module-owned (scoped)

  • middleware is attached when a module/registrator activates

  • it is removed when the module/registrator deactivates

  • ideal for feature modules and isolation

  1. Test-owned

  • middleware is attached during Arrange

  • removed/disposed during teardown

  • prevents test pollution

Rule of thumb:

  • If you need isolation, choose module-owned or test-owned.

  • If you need universal observability, choose app-owned.

3) Put all long-lived cleanup in dispose()

Anything that can outlive a single message processing must be cleaned in dispose.

Checklist for dispose:

  • unsubscribe from all subscriptions created by middleware

  • clear intervals/timeouts

  • remove event listeners

  • clear internal maps that could hold references

Best practice:

  • make dispose idempotent (safe if called once; avoid throwing)

4) Design per-operation state so it cannot leak

Because after is not guaranteed (cancellation or failures), avoid designs that require after to always run.

Safe patterns:

  • No per-operation state: counters only, no maps.

  • Bounded map: Map keyed by message id with TTL/LRU cleanup.

  • Stage-limited state: only track timing for Execute stage (simpler, fewer cases).

  • Try-finally at the boundary (if you instrument outside middleware): when you own the call site.

Anti-pattern:

  • unbounded Map keyed by message id with cleanup only in after.

5) Keep canHandle cheap and avoid accidental activation

Overly broad canHandle causes:

  • unnecessary computation,

  • more state allocations,

  • harder-to-debug leaks.

Do:

  • filter by stage first

  • then by message subset

6) Verify teardown is actually happening

Many leaks are not in middleware code, but in lifecycle wiring:

  • middleware attached but never removed

  • bus never disposed in tests

  • multiple bus instances created across reloads without teardown

Ensure you have a teardown strategy and that it runs:

  • app shutdown hook (if applicable)

  • module deactivation hook (if applicable)

  • test afterEach/afterAll cleanup

Verification (expected outcome)

Checklist

After a test/module teardown:

  • middleware dispose has run

  • middleware-owned subscriptions are closed

  • timers are cleared

  • internal maps are empty (or small and bounded)

During runtime:

  • memory does not grow linearly with number of processed messages

  • logging/metrics middleware overhead remains stable

Diagram: resource lifecycle ownership

@startuml title Resource ownership and disposal points

actor Owner participant "Postboy bus" as Bus participant "Middleware instance" as MW participant "Resources\n(subscriptions/timers/maps)" as R

Owner -> Bus: create bus Owner -> MW: create middleware Owner -> Bus: attach middleware Bus -> MW: middleware active

MW -> R: allocate long-lived resources (optional) note over MW: per-operation temp state\nmay be allocated in before

Owner -> Bus: remove middleware OR dispose bus Bus -> MW: dispose() MW -> R: release resources MW -> MW: clear internal state

@enduml

Troubleshooting

“My middleware leaks memory over time”

Most common causes:

  1. Per-operation Map keyed by message id grows without bounds.

  2. after is skipped due to cancellations, so cleanup never happens.

  3. Timers/intervals are created and never cleared.

  4. Subscriptions are created and never unsubscribed.

Fix steps:

  • Remove message id from metric labels and avoid storing large per-message state.

  • Add TTL/LRU cleanup to any map.

  • Move all long-lived cleanup to dispose.

  • Reduce scope via canHandle.

“Tests affect each other”

Cause:

  • middleware attached globally and not removed/disposed between tests.

Fix steps:

  • Attach middleware per test (Arrange).

  • Ensure teardown removes middleware or disposes the bus.

  • Avoid global singleton bus in tests unless you have strict reset logic.

“dispose() is never called”

Cause:

  • middleware is never removed and the bus is never disposed.

Fix:

  • Add an explicit teardown step in your runtime (or in tests).

  • If your app architecture does not have teardown, prefer middleware with no long-lived resources.

Best practices (copy-safe rules)

  • Treat middleware as a long-lived object: anything it allocates must be releasable in dispose.

  • Do not rely on after for cleanup (it’s not guaranteed on cancel).

  • Use bounded state for per-operation tracking.

  • Keep filtering tight; do not run everywhere by default.

  • Prefer composition:

    • one middleware for logging

    • one for metrics

    • one for guards This makes lifecycle and ownership clearer.

Next steps

  • [Middleware lifecycle and contract (before/after/canHandle/dispose)]

  • [Interrupt/cancel semantics (why after() may not run)]

  • [How-to: measure duration and collect metrics]

  • [How-to: logging and tracing messages]

  • [Troubleshooting: diagnosing leaks and unexpected middleware behavior]

29 июня 2026