Skip to content

Trigger never deletes the destination it provisions — 49 orphaned in 9 days #13

Description

@leggetter

Summary

The trigger creates a Hookdeck destination on activation and never deletes it. On
deactivation it pauses, disables or deletes the connection — the destination it provisioned
is always left behind. Destinations therefore accumulate permanently in a user's project, one
(two with the -test variant) per workflow-and-node, for the lifetime of the project.

This is user-facing, not just a test-suite problem. The live suite has the same blind spot in
its own cleanup, which adds to the pile — covered separately below.

Evidence

A real Event Gateway project used for development over nine days, snapshotted before cleanup:

destinations total          : 54
  node-provisioned          : 53
  referenced by a connection: 4
  ORPHANED (no connection)  : 49
  accumulated between       : 2026-08-12 and 2026-08-21
connections total           : 10

49 of 53 node-provisioned destinations had no connection pointing at them. Nothing removes
them, and nothing surfaces them — they are invisible until you list destinations.

Two of them were the pair behind a single workflow:

n8n-dest-pQYxZBBy5lw1X2Ib-…-test  type=CLI  created 17:07:00   <- connection already gone
n8n-dest-pQYxZBBy5lw1X2Ib-…       type=CLI  created 17:06:55

Cause

webhookMethods.default.delete in nodes/Hookdeck/HookdeckEventGatewayTrigger.node.ts acts
only on the connection:

  • pause (the default for a production registration) — PUT /connections/:id/pause
  • disable (a test registration delivering over the CLI) — PUT /connections/:id/disable
  • delete (a test registration over HTTP, or onDeactivate: delete) — DELETE /connections/:id

grep -rn "destinations/" nodes/ | grep -i delete returns nothing. The node issues no
DELETE /destinations/:id anywhere.

For pause and disable that is correct — the connection survives, so its destination must
too. The leak is the delete path: the connection goes and its destination is orphaned
immediately.

Suggested fix

Delete the destination alongside the connection on the delete path only. Destination names
are derived per workflow-and-node (n8n-dest-<workflowId>-<nodeId>, plus the -test
variant), so they are 1:1 with the connection and safe to remove together — but that
assumption is worth asserting rather than trusting, since deleting a shared destination would
break another connection.

Points worth deciding:

  • Ordering. Delete the connection first, then the destination; a destination still
    referenced by a connection will refuse to delete.
  • A 404 on the destination is success, exactly as the connection delete already treats it.
  • Existing users have a backlog. A fix stops the bleeding but does not clear the 49
    already there. Worth a note in the release, since the only remedy is manual, and possibly
    worth a hookdeck gateway destination list one-liner in the README.
  • Failure handling. The connection delete deliberately surfaces non-404 errors. A failed
    destination delete is less serious — the connection is already gone, so nothing is
    delivering — and arguably belongs as a warning rather than a thrown error, so teardown is
    not blocked by it.

Related: the live suite has the same blind spot

cleanUpRun in test/live/_harness.mjs filters on ownedByThisRun, which matches three name
shapes:

name.startsWith(PREFIX) || name.startsWith(`cli-${PREFIX}`) || name.startsWith(`n8n-${RUN_ID}-`)

The node names destinations n8n-dest-<run>-<node>, which matches none of them, so every
node-provisioned destination survives the sweep. Measured across one run: sources 8 → 8 and
connections 9 → 9 (clean), destinations 27 → 39.

This is the same bug the function's own comment already records for connections:

The third was missing at first, so every node-provisioned connection survived cleanup and
then blocked its source from being deleted.

Fixed there, missed for destinations. Note the comment's warning that widening the filter
starts deleting real resources — the fix should add the n8n-dest-<RUN_ID>- shape
specifically, not a looser prefix.

Notes

Found while verifying #10 and confirmed while clearing the development project down. The 73
resources deleted during that cleanup were snapshotted first; the counts above come from that
snapshot. Not fixed in #10 — unrelated to it, and it touches deletion logic against a live
project, which deserves its own PR and its own live-suite assertion.

A good regression test: assert after a delete-path teardown that the destination is gone,
not just the connection. The current suite would pass today with the leak in place.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions