skip to content

Shared CI Instances

One long-lived stub server serving a whole pipeline or environment, started beside the suite and polled until it answers. Another team's run can then see and change your stubs.

on this pageshow

explore

questions

4

In WireMock, what happens once --max-request-journal-entries is exceeded on a shared instance?

level: seniorimportance: must knowfreq 49%

answer

  1. a bound, not a switch
  2. eviction, never refusal
  3. the oldest go first
  4. no error and no warning
  5. counts come back too small

basics

~20 s

WireMock's journal is bounded once --max-request-journal-entries is set, and passing the bound silently evicts the oldest entries. Nothing errors and nothing warns. A count taken afterwards under-reports what a long-lived shared instance received, so the answer is wrong, not missing.

solid answer

~40 s

A long-lived WireMock accumulates journal entries for every run that touches it, so operators bound it with `--max-request-journal-entries`. The bound is enforced by **eviction, not refusal**: once the instance holds more entries than the cap allows, WireMock discards the **oldest** ones and carries on serving. There is no error, no rejected request and nothing in the reply your client receives. The damage appears later and quietly, because a count taken over a journal that has already rolled reports fewer requests than actually arrived, so a bakery pre-order suite that placed three orders against `POST /preorders/v1/orders` can be told two. On a shared instance the traffic that pushed the bound over is usually somebody else's run, so identical code passes and fails by coincidence, which is exactly what a team labels flaky and stops investigating.

code

bash · 7 lines
bash
# One long-lived WireMock for the whole bakery pre-order pipeline
docker run -d --name bakery-preorder-stubs -p 8080:8080 \
  wiremock/wiremock \
  --max-request-journal-entries 2000

# Past 2000 retained entries the oldest are dropped, silently, while
# the instance keeps answering /preorders/v1/orders exactly as before

go deeper

for a junior

Know that WireMock keeps an in-memory record of the requests it served, and that a start-up flag bounds how many entries it retains. Be able to say the bound counts entries, not minutes.

for a middle

Explain the mechanic precisely. --max-request-journal-entries evicts the oldest entries rather than rejecting requests, so the instance keeps serving normally and nothing in the client's reply changes.

for a senior

Demonstrate the diagnosis. Explain why counts under-report on a long-lived instance, why the loss lands on the start of your own run, and why the trigger is usually another team's traffic.

for a principal

Own the sizing rule across teams: how the cap is derived from aggregate traffic, who re-derives it when a pipeline joins, and how you stop a silent eviction from being discovered by a flaky suite months later.

## The journal on an instance nobody restarts WireMock records the requests it serves into an in-memory **request journal**. On an instance created for one test run that journal is bounded by the run itself: it starts empty, grows for a few minutes and disappears when the process exits. A **shared CI instance** is the opposite shape. One long-lived `wiremock/wiremock` container stands beside the pipeline, serves the bakery pre-order API to every suite that points at it, and is left running for days or weeks. Nothing about that arrangement bounds the journal, so it grows for as long as the container has been up and across every team's traffic. `--max-request-journal-entries` is the flag that puts a ceiling on it, and it is chosen when the instance starts. ## The cap is enforced by eviction, not by refusal This is the part that matters and the part people get wrong. The flag sets a maximum number of **retained entries**. When the instance has recorded more than that, WireMock does not reject the request, does not stop serving, does not fail the call and does not put anything in the reply. It drops the **oldest** entries to make room for the new one and carries on. The client that triggered the eviction receives exactly the stubbed response it would have received anyway. That design is defensible. An unbounded in-memory journal inside a process that runs for weeks is a slow leak, and a stub server that started refusing traffic to protect its own bookkeeping would be a worse neighbour. But the failure mode it produces has three properties worth naming: - it is **silent** — no exception, no rejected request, no header, no marker anywhere in the response; - it is **lossy at the far end** — the entries that vanish are the earliest ones, which on a long-lived instance are usually from the beginning of the current run; - it is **driven by traffic you do not control** — on a shared instance the requests that pushed the journal past its bound frequently belong to another team's pipeline entirely. ## Why this produces a wrong answer rather than a missing one A check that consults the journal after the bound has been crossed does not receive an error telling it the history is incomplete. It receives a number, and the number is too small. Suppose a bakery pre-order suite places three orders against `POST /preorders/v1/orders` and then asks how many arrived. If the journal rolled between the first order and the question, the answer comes back as two. The suite fails on a count, the application code is correct, and the reason is invisible from inside the test. The inverse is worse. A check that asserts something did **not** happen, such as no request reaching a cancellation path, is satisfied by an empty result, and eviction manufactures empty results. That check then passes for a reason with nothing to do with the behaviour it claims to protect. | behaviour | shared long-lived instance | instance created per run | |---|---|---| | journal grows without bound | yes, until the cap bites | no, it dies with the run | | oldest entries evicted mid-run | yes | effectively never | | eviction triggered by unrelated traffic | yes | no | | history-based checks answer wrongly | yes | no | | the failure reproduces on rerun | rarely | not applicable | ## Sizing the cap when the instance is shared 1. **Measure aggregate traffic, not your own.** The number that matters is what every pipeline pointing at this instance sends together, over the longest window any single run needs history for. 2. **Add real headroom.** Retained entries are cheap next to a wrong answer. A cap you never reach costs a little memory; a cap you cross costs correctness and a week of chasing flake. 3. **Re-derive it when the population changes.** A new team pointing a suite at the shared instance changes the arithmetic for everybody already on it, and nothing in the system will tell you that it did. 4. **Write the value down where the instance is defined**, next to the reason for it. A bare number nobody can explain gets halved by the next person trying to save memory. ## What the cap cannot be asked to do - It cannot make history-based checks safe on a shared instance; it can only move the point at which they stop being safe. - It cannot distinguish your run's entries from anybody else's, because the journal is one sequence and eviction walks it from the front. - It cannot be varied for one suite. It is a property of the instance, fixed when the process starts, and shared by every consumer of that instance. - It cannot warn you. If you need to know the bound was crossed, the instance's own traffic volume is the only thing to watch, and you have to watch it deliberately. The honest summary is that `--max-request-journal-entries` is a memory control with a correctness side effect, on exactly the hosting shape where it is most necessary. On a shared instance, treat any check that depends on request history as a check whose truthfulness rests on a number somebody chose months ago.

  • How would you choose a value for --max-request-journal-entries on a shared instance?
    Derive it from aggregate traffic rather than from one suite. Take what the busiest concurrent set of runs sends together over the longest window any single run needs history for, add generous headroom, and re-derive it whenever a new pipeline starts pointing at the instance. A cap you never reach costs only memory; one you cross costs correctness.
  • Why does eviction damage a run's earliest requests specifically?
    Because the cap discards the oldest retained entries first. On a long-lived instance the oldest entries at the moment the bound is crossed are typically the ones recorded at the start of the current run, so a suite loses exactly the setup traffic it is least likely to suspect when it begins debugging.
  • What makes this failure look intermittent rather than reproducible?
    The trigger is not in your code. Whether the bound is crossed during your run depends on how much traffic other pipelines happened to send to the same instance in the same window, so identical commits pass and fail by coincidence of scheduling rather than by anything the suite did.

saying these in an interview costs you the question

  • Says WireMock rejects requests once the journal cap is reached.
  • Expects an error or a warning when entries are evicted.
  • Thinks the newest entries are dropped and the earliest history survives.
  • Assumes the cap only limits memory and cannot change what a count reports.
  • Blames the suite for flakiness when another team's traffic caused the eviction.
open as a page

One long-lived WireMock serves every team's pipeline. When has that shape stopped paying for itself?

level: principalimportance: must knowfreq 52%

basics

~20 s

A shared WireMock has outgrown its pipeline once its singular surfaces collide. It keeps one journal whose cap other teams exhaust, one admin API any job can rewrite, and one health signal that hides both. Measure those before deciding.

open as a page

What does WireMock's GET /__admin/health tell a CI job waiting on a shared stub server?

level: middleimportance: should knowfreq 64%

basics

~20 s

WireMock's GET /__admin/health answers 200 only once the admin API is serving, so a pipeline polls it rather than the TCP port. It proves the process is up. It does not prove your bakery pre-order mappings are loaded.

open as a page

What does WireMock's --admin-api-basic-auth protect on a shared CI instance, and what does it leave open?

level: seniorimportance: should knowfreq 46%

basics

~20 s

WireMock's --admin-api-basic-auth makes the whole /__admin surface demand credentials. Nothing on the network can then silently rewrite a shared instance's stubs. It partitions nothing, though: every job holding that one credential still shares a single mapping set.

open as a page