skip to content

In mountebank, what happens to an imposter's port when a parallel worker finishes or dies?

level: middleimportance: should knowfreq 48%

answer

  1. sockets outlive the worker that made them
  2. nothing reclaims a port on its own
  3. the imposter's identity is its port
  4. DELETE /imposters/<port> closes the socket
  5. GET /imposters shows what is still held

basics

~20 s

Nothing releases it automatically. One mb process holds every imposter's listening socket for as long as the imposter exists, so a worker that dies without deleting its imposter leaves the port bound on that machine until mb itself exits.

solid answer

~50 s

Nothing reclaims it on its own. A single `mb` process owns every imposter it has created, and each imposter is a listening socket that stays bound for as long as the imposter exists — worker exit, test failure and suite end do nothing to it. In mountebank an imposter is addressed **by its port**, so the number you read back is also the handle you clean up with: `DELETE /imposters/<port>` removes the imposter and closes its socket. `GET /imposters` shows what is still held, which is how you tell a leak from a fresh collision. A run in which every worker lets mountebank allocate survives leaks, because the next worker simply gets a different number; a run with a fixed per-worker port table does not, because the number it insists on may still be bound. In WireMock the equivalent release is stopping the embedded `WireMockServer`.

code

bash · 7 lines
bash
MB="http://localhost:${MB_PORT}"

# what this mb process is still holding
curl -s "$MB/imposters" | jq -r '.imposters[] | .port'

# release this worker's port; an imposter is addressed by that port
curl -s -X DELETE "$MB/imposters/${PORT}"

go deeper

for a junior

Know that deleting an imposter is what closes its socket, and that an imposter is addressed by its port in mountebank's control API rather than by a separate identifier.

for a middle

Explain that one mb process holds every imposter's socket, that nothing reclaims one when a worker exits, and what GET /imposters tells you about what is still bound.

for a senior

Describe how you make teardown survive a crashed worker, and how you choose between sweeping strays at run start and starting a fresh mb process instead.

for a principal

Own the lifecycle policy: who is responsible for releasing a port, what a long-lived mb process may accumulate, and how a leak becomes visible before it becomes a failed run.

## One process, many listening sockets It is easy to picture a mountebank imposter as a little server that lives and dies with the worker that asked for it. It does not. One `mb` process holds all of them: every imposter created against that process is a listening socket owned by `mb`, and it stays open for as long as the imposter is registered. The worker that created it is just a client that made an HTTP call. That has one consequence worth internalising before anything else. **The worker's lifetime and the port's lifetime are unrelated.** A worker that finishes cleanly, a worker whose test failed, a worker killed by the runner, a worker whose process crashed — none of those events reach `mb`, and none of them close the socket serving your marina berth-booking stubs. ## An imposter's identity is its port Mountebank's control API addresses an imposter by the port it listens on. That is a small design detail with a useful practical consequence in a parallel run: the number you read back out of the creation response is simultaneously - the address you give the code under test for `GET /berths` and `POST /berths/{berthId}/bookings`; - the identifier in the control-API path when you want to act on that imposter; - the thing you compare against `GET /imposters` to see whether it is still held. So a worker does not need to remember an extra handle. If it kept the port, it kept everything it needs to clean up. ## What actually releases the socket - `DELETE /imposters/<port>` removes the imposter and closes its listener. This is the operation that frees the number. - Stopping the `mb` process releases every imposter it held, all at once. This is the blunt version and it is often the right one in CI, where `mb` is disposable. - Nothing else does. A worker exiting, a test failing, a suite finishing, an assertion blowing up mid-run: none of these touch the imposter. And one thing that looks like it should and does not: clearing what an imposter has recorded is a different operation on a different sub-resource, and it leaves the listener exactly where it was. Freeing a port and resetting state are separate concerns; do not reach for one expecting the other. ## Why an allocated run survives a leak and a fixed-port run does not This is the payoff of letting mountebank choose. Suppose a worker crashed last night and its imposter is still registered on some number. 1. In a run where each worker posts an imposter with **no `port` field**, mountebank simply binds a different free port for the new worker. The leak wastes a socket and nothing else; the run is unaffected. 2. In a run where each worker takes a **declared number** from a per-worker table, the worker assigned the leaked number cannot bind, and the run fails for a reason that has nothing to do with the test. 3. Worse, if the leaked imposter belongs to a *different* suite, the declared-port worker may not fail at all — it may find the port already answering and never notice that the marina-berth replies are somebody else's stubs. Case three is the dangerous one, because it produces a green or red result that is unrelated to the code under test. ## Diagnosing what is still held When a run collides or an imposter answers with a body you never wrote, work in this order: 1. List the imposters: `GET /imposters` returns every one the `mb` process currently holds, with its `port` and `name`. 2. Look for ports this run did not create. Those belong to a leaked worker or to another suite on the machine. 3. Look for imposters whose `name` you recognise from an older run — a naming convention that includes the run or worker identity makes this a two-second check instead of a guess. 4. Decide between deleting the strays by port and restarting `mb` entirely, based on whether the process is yours to restart. ## Teardown that survives a crash - Delete the imposter at worker teardown, and make that step run even when the test body failed. - Do not rely on teardown alone: a killed process runs no teardown, so the run must also tolerate strays. - Toleration means allocation, not cleverness — a run whose ports are outputs cannot be blocked by a leftover. - Prefer a fresh `mb` process per run where you control it. It is the cheapest guarantee of a clean port space. - If `mb` is long-lived, sweep at run start rather than at run end, because the run that leaked is by definition the one that did not get to clean up.

  • Why does a leaked imposter hurt a fixed-port scheme more than an mb-allocated one?
    An allocated run simply gets a different free number from mountebank and carries on. A fixed-port run insists on one specific number that the leaked imposter is still holding, so it either fails to bind or — if the leak belongs to another suite — reaches a stub set it did not create and asserts against the wrong replies.
  • How do you start a run when a previous run left imposters behind?
    List them with `GET /imposters` and delete the strays by `DELETE /imposters/<port>`, or start a fresh `mb` process and treat the old one as disposable. What you must not do is assume a clean slate. The fresh process is usually cheaper in CI; the sweep is for when `mb` is not yours to restart.

saying these in an interview costs you the question

  • Assumes an imposter disappears when the worker exits
  • Deletes an imposter's saved requests and expects the port freed
  • Restarts the whole mb process between individual tests
  • Cannot say what GET /imposters would show after a crash
  • Treats a leaked port as an operating-system problem