Repository: fluent/fluent-bit (2026-07-02T05:45:33Z)

Issue:
Silent record loss in rewrite_tag when the shared in_emitter is paused (FLB_EMITTER_BUSY / -2 not handled)
## Bug Report

**Describe the bug**

`rewrite_tag` silently and permanently loses records when its `in_emitter` is paused (`Emitter_Mem_Buf_Limit` reached).

When the emitter is paused, `in_emitter_add_record()` refuses the record — correct back-pressure — returning `FLB_EMITTER_BUSY` (`-2`) without buffering it (`plugins/in_emitter/emitter.c:169`). The bug is in the caller: `process_record()` in `plugins/filter_rewrite_tag/rewrite_tag.c` only checks `if (ret == -1)` (line 417), so `-2` falls through to `return FLB_TRUE` (line 421) — the refused emit is treated as a **success**. With `keep_record=false` (the trailing `false` on a `Rule`), the original record is then dropped (line 505). The re-emitted copy was never buffered and the original is discarded — the record is gone, with only a debug-level log line and no drop metric.

```c
/* rewrite_tag.c, process_record() */
ret = in_emitter_add_record(out_tag, flb_sds_len(out_tag), buf, buf_size,
                            ctx->ins_emitter, i_ins);   /* can return FLB_EMITTER_BUSY (-2) */
...
if (ret == -1) {      /* line 417 — misses -2 */
    return FLB_FALSE;
}

return FLB_TRUE;      /* -2 lands here: "emitted" */
```

The `-2` return was introduced by #8473 (v3.0.2), which correctly back-pressures *streaming* reads by pausing the source input — but any record that reaches the emitter outside the pausable read loop hits this loss path. The most reliable trigger: **two tails sharing one emitter**, where a high-volume tail keeps the emitter paused and a low-volume multiline tail's **flush-timeout** fires (timer-driven, so the pause can't hold it back — the line is already consumed past the tail's read offset, so there is no on-disk copy to re-read on resume).

Related: #8198 (closed by #8473; this is the residual loss path the merged fix does not cover).

**To Reproduce**

- Steps to reproduce the problem:
  1. Save the self-contained script below as `repro.sh` (needs only `docker`/`podman` + `python3`; runs stock `fluent/fluent-bit:5.0.3`).
  2. Run `RUNS=3 bash repro.sh`.
  3. Two tails share one `rewrite_tag` emitter (`Emitter_Mem_Buf_Limit 1M`): a `noise` tail keeps it paused, a `victim` multiline tail writes numbered lines in two classes — `follower` (flushed by the next line arriving, i.e. read-coupled) and `timeout` (flushed by the 1s multiline `flush_timeout`). The script tallies delivery per class against a ground-truth ledger.

Result on stock `fluent/fluent-bit:5.0.3` — reproduces in **every** run:

```
===== run 1/3 =====
  follower : 80/80 delivered (100.00%)
  timeout  : 72/80 delivered (90.00%)
             LOST: 000047 000051 000055 000059 000063 000067 000071 000075
  RESULT: timeout_lost=8 follower_lost=0
...
Reproduced timeout-flushed loss in 3/3 run(s).
```

The lost seqs are periodic — they are the ones whose flush timer fires inside a paused-emitter window. `follower` = 100% across all runs, `timeout` ≈ 73–90%. No metric reports the drops; the only trace is the debug line `emitter memory buffer limit reached. Not accepting record.`

<details><summary><code>repro.sh</code> (self-contained, generates its own config)</summary>

```bash
#!/usr/bin/env bash
# Self-contained reproducer for the in_emitter FLB_EMITTER_BUSY (-2) silent
# record-loss bug in Fluent Bit's rewrite_tag filter.
#
# Bug: when a rewrite_tag rule re-emits a record into a shared in_emitter
# whose mem-buf-limit is already reached, in_emitter_add_record() returns
# FLB_EMITTER_BUSY (-2) WITHOUT buffering the record. rewrite_tag.c only
# checks `if (ret == -1)`, so the -2 falls through, the filter reports the
# record as emitted, and with keep_record=false (the trailing `false` on the
# Rule) the original is dropped from the stream. Net effect: permanent,
# silent loss of that record.
#
# Why a TIMEOUT-flushed multiline record specifically: #8473's back-pressure
# pauses the SOURCE tail when the emitter is over its limit, which stops
# read-coupled flushes. But a multiline TIMEOUT flush is driven by an idle
# TIMER, not by a read, so it fires while the tail is paused and reaches the
# still-paused emitter -> hits the -2 path. Read-coupled "follower" flushes
# (flushed because the next start line arrived) only happen while reading,
# i.e. while the emitter has room, so they are never lost.
#
# Two containers share a volume:
#   writer (alpine)      -- floods a noise file to keep the shared emitter at
#                           its limit, and single-writes victim burst+idle
#                           pairs so each pair's trailing line is timeout-flushed.
#   fluent-bit (stock)   -- tails both files; both inputs re-emit through ONE
#                           rewrite_tag/in_emitter; victim -> stdout, noise -> null.
# The host then tallies victim delivery by flush class from the fb stdout.
#
# Usage:
#   bash repro.sh                 # 1 run, defaults
#   RUNS=5 bash repro.sh          # 5 runs, report repro rate
#   CYCLES=200 bash repro.sh      # more cycles -> more events
#   IMAGE=fluent/fluent-bit:latest bash repro.sh
#   RUNTIME=docker bash repro.sh  # force docker over podman
#
# Exit status: 0 if no timeout-flushed loss in any run, 1 if loss occurred.
set -euo pipefail

IMAGE="${IMAGE:-docker.io/fluent/fluent-bit:5.0.3}"
RUNS="${RUNS:-1}"
CYCLES="${CYCLES:-150}"          # victim burst+idle pairs per run
IDLE="${IDLE:-1.2}"             # idle after the timeout line (must exceed 1s flush_timeout)
RECV_GAP="${RECV_GAP:-0.007}"   # gap between the follower line and the timeout line
DRAIN="${DRAIN:-8}"             # seconds after last line for the final flushes to land
FLUSH="${FLUSH:-5}"             # engine flush; re-emitted chunks sit this long between drains
EMITTER_LIMIT="${EMITTER_LIMIT:-1M}"  # shared in_emitter mem-buf-limit; lower = easier to pause
NOISE_SIZE="${NOISE_SIZE:-65536}"     # bytes per noise line
NOISE_BURST="${NOISE_BURST:-16}"      # noise lines per burst
NOISE_GAP="${NOISE_GAP:-0.3}"         # seconds between noise bursts

# Resolve a real container runtime binary (docker is often a podman alias that
# subprocess/scripts can't see; prefer an explicit override, then docker, then podman).
RUNTIME="${RUNTIME:-}"
if [[ -z "$RUNTIME" ]]; then
    for c in docker podman; do
        if command -v "$c" >/dev/null 2>&1; then RUNTIME="$c"; break; fi
    done
fi
if [[ -z "$RUNTIME" ]]; then
    echo "ERROR: neither docker nor podman is on PATH" >&2
    exit 2
fi

CFG_DIR="$(mktemp -d)"
trap 'rm -rf "$CFG_DIR"' EXIT

# --- generated config: minimal, generic, no project-specific naming ---------
cat > "$CFG_DIR/parsers.conf" <<'PARSERS'
# Each victim line starts with a "[YYYY-MM-DD HH:MM:SS" preamble, so every
# line is a multiline start_state -> its own one-line record. The previous
# record flushes when the next start line arrives (follower) or, for the last
# line before an idle gap, when flush_timeout (1000ms) fires (timeout).
[MULTILINE_PARSER]
    name           repro
    type           regex
    flush_timeout  1000
    rule  "start_state"  "/^\[\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}/"  "cont"
    rule  "cont"         "/^(?!\[\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/"  "cont"
PARSERS

cat > "$CFG_DIR/fluent-bit.conf" <<'FLUENTBIT'
[SERVICE]
    Flush         ${FLUSH}
    Daemon        Off
    Log_Level     info
    Parsers_File  /cfg/parsers.conf

# Victim: multiline tail. Single-writer burst+idle pairs make each pair's
# trailing line a lone timeout-flushed single-record chunk.
[INPUT]
    Name              tail
    Tag               in.victim
    Path              /data/victim.log
    DB                /data/victim.db
    Read_from_Head    true
    Buffer_Max_Size   1024k
    Skip_Long_Lines   On
    multiline.parser  repro
    multiline_flush   1

# Aggressor: plain high-volume tail. Its only job is to keep the shared
# in_emitter at its mem-buf-limit while the victim trickles. Buffer_Max_Size
# MUST exceed NOISE_SIZE (default 64KB); the tail default is 32KB, so without
# this the big noise lines hit Skip_Long_Lines and never pressure the emitter.
[INPUT]
    Name              tail
    Tag               in.noise
    Path              /data/noise.log
    DB                /data/noise.db
    Read_from_Head    true
    Buffer_Max_Size   1024k
    Skip_Long_Lines   On

[FILTER]
    Name    modify
    Alias   tag-victim
    Match   in.victim
    Add     kind  victim

[FILTER]
    Name    modify
    Alias   tag-noise
    Match   in.noise
    Add     kind  noise

# The stage under test: BOTH inputs re-emitted through ONE in_emitter.
# keep_record=false (trailing `false`) means the original is dropped once the
# filter believes the re-emit succeeded -- including when it actually got -2.
[FILTER]
    Name                   rewrite_tag
    Alias                  retag-shared-emitter
    Match                  in.*
    Rule                   $kind ^(.+)$ out.$1 false
    Emitter_Name           shared_emitter
    Emitter_Mem_Buf_Limit  ${EMITTER_LIMIT}

# Victim -> stdout (the host tallies seq=/cls= here). Noise -> discard.
[OUTPUT]
    Name              stdout
    Match             out.victim
    Format            json_lines

[OUTPUT]
    Name    null
    Match   out.noise
FLUENTBIT

# In-container writer (POSIX sh, runs in the alpine writer container).
cat > "$CFG_DIR/writer.sh" <<'WRITER'
#!/bin/sh
set -eu
V=/data/victim.log
N=/data/noise.log
L=/data/ledger.tsv
: > "$V"; : > "$N"; : > "$L"

noise_line=$(head -c "$NOISE_SIZE" /dev/zero | tr '\0' x)
(
  while true; do
    n=0
    while [ "$n" -lt "$NOISE_BURST" ]; do
      printf '%s\n' "$noise_line" >> "$N"
      n=$((n + 1))
    done
    sleep "$NOISE_GAP"
  done
) &
NOISE_PID=$!

i=1
while [ "$i" -le "$CYCLES" ]; do
  seq=$(printf '%06d' "$i")
  ts="[$(date '+%Y-%m-%d %H:%M:%S')]"
  printf '%s seq=%s cls=follower MSG ping received\n' "$ts" "$seq" >> "$V"
  printf '%s\tfollower\n' "$seq" >> "$L"
  sleep "$RECV_GAP"
  ts="[$(date '+%Y-%m-%d %H:%M:%S')]"
  printf '%s seq=%s cls=timeout MSG ping succeeded\n' "$ts" "$seq" >> "$V"
  printf '%s\ttimeout\n' "$seq" >> "$L"
  sleep "$IDLE"
  i=$((i + 1))
done

kill "$NOISE_PID" 2>/dev/null || true
sleep "$DRAIN"
WRITER

# Host-side analyzer: tally victim delivery by flush class.
cat > "$CFG_DIR/tally.py" <<'TALLY'
import json, re, sys
ledger_path, stdout_path = sys.argv[1], sys.argv[2]
pat = re.compile(r"seq=(\d{6}) cls=(follower|timeout)")
emitted = {"follower": set(), "timeout": set()}
with open(ledger_path) as f:
    for line in f:
        line = line.strip()
        if not line:
            continue
        seq, cls = line.split("\t")
        emitted[cls].add(seq)
delivered = {"follower": set(), "timeout": set()}
with open(stdout_path) as f:
    for line in f:
        line = line.strip()
        if not line:
            continue
        try:
            rec = json.loads(line)
        except json.JSONDecodeError:
            m = pat.search(line)
            if m:
                delivered[m.group(2)].add(m.group(1))
            continue
        m = pat.search(rec.get("log", ""))
        if m:
            delivered[m.group(2)].add(m.group(1))
lost = {c: sorted(emitted[c] - delivered[c]) for c in emitted}
# Drop the final-cycle line: it can be cut off by container teardown, which is
# a harness artifact, not the emitter bug.
last = "%06d" % max((int(s) for c in emitted for s in emitted[c]), default=0)
for c in lost:
    lost[c] = [s for s in lost[c] if s != last]
for c in ("follower", "timeout"):
    e = len(emitted[c]); l = len(lost[c]); d = e - l
    rate = (d / e * 100) if e else 0.0
    print(f"  {c:9s}: {d}/{e} delivered ({rate:.2f}%)")
    if lost[c]:
        prev = " ".join(lost[c][:20])
        more = "" if len(lost[c]) <= 20 else f" ... (+{len(lost[c]) - 20} more)"
        print(f"             LOST: {prev}{more}")
t_lost = len(lost["timeout"]); f_lost = len(lost["follower"])
print(f"  RESULT: timeout_lost={t_lost} follower_lost={f_lost}")
sys.exit(1 if t_lost else 0)
TALLY

run_once() {
    run_idx="$1"
    vol="fbrepro_$$_${run_idx}"
    fbname="fbrepro_fb_$$_${run_idx}"
    "$RUNTIME" volume create "$vol" >/dev/null

    # Start stock Fluent Bit, tailing the (soon-to-be-written) files on the volume.
    "$RUNTIME" run -d --name "$fbname" \
        -e FLUSH="$FLUSH" -e EMITTER_LIMIT="$EMITTER_LIMIT" \
        -v "$vol:/data" -v "$CFG_DIR:/cfg:ro" \
        "$IMAGE" /fluent-bit/bin/fluent-bit -c /cfg/fluent-bit.conf >/dev/null
    sleep 3   # let the tail watcher initialize before any write

    # Writer container: floods noise + single-writes victim pairs, then drains.
    "$RUNTIME" run --rm \
        -e CYCLES="$CYCLES" -e IDLE="$IDLE" -e RECV_GAP="$RECV_GAP" \
        -e DRAIN="$DRAIN" -e NOISE_SIZE="$NOISE_SIZE" \
        -e NOISE_BURST="$NOISE_BURST" -e NOISE_GAP="$NOISE_GAP" \
        -v "$vol:/data" -v "$CFG_DIR:/cfg:ro" \
        docker.io/library/alpine:3 sh /cfg/writer.sh

    # Collect fb stdout and the ground-truth ledger.
    "$RUNTIME" logs "$fbname" > "$CFG_DIR/fb.stdout.${run_idx}" 2>/dev/null
    "$RUNTIME" run --rm -v "$vol:/data" docker.io/library/alpine:3 \
        cat /data/ledger.tsv > "$CFG_DIR/ledger.tsv.${run_idx}" 2>/dev/null

    "$RUNTIME" rm -f "$fbname" >/dev/null 2>&1 || true
    "$RUNTIME" volume rm "$vol" >/dev/null 2>&1 || true
}

echo "Runtime: $RUNTIME"
echo "Image:   $IMAGE"
echo "Runs:    $RUNS   Cycles/run: $CYCLES   Emitter limit: $EMITTER_LIMIT   Flush: ${FLUSH}s"
echo

runs_with_loss=0
for r in $(seq 1 "$RUNS"); do
    echo "===== run $r/$RUNS ====="
    run_once "$r"
    if python3 "$CFG_DIR/tally.py" "$CFG_DIR/ledger.tsv.${r}" "$CFG_DIR/fb.stdout.${r}"; then
        :
    else
        runs_with_loss=$((runs_with_loss + 1))
    fi
    echo
done

echo "========================================================"
echo "Reproduced timeout-flushed loss in ${runs_with_loss}/${RUNS} run(s)."
[ "$runs_with_loss" -gt 0 ] && exit 1 || exit 0
```

</details>

**Expected behavior**

A record refused by a paused emitter should not be lost. Either the original should be preserved (e.g. force `keep = FLB_TRUE` for that record so it continues under its original tag), or the busy condition should propagate as back-pressure/retry — consistent with the intent of #8473. At minimum, `FLB_EMITTER_BUSY` must not be indistinguishable from a successful emit at `rewrite_tag.c:417`.

**Screenshots**

N/A (see measured output above).

**Your Environment**

* Version used: 5.0.3 (`fluent/fluent-bit:5.0.3`); the same code path is present on current `master` (identical line numbers)
* Configuration: two `tail` inputs sharing one `rewrite_tag` emitter (`Emitter_Mem_Buf_Limit 1M`, `keep_record=false`); full config generated by the reproducer script above
* Environment name and version: Docker / Podman containers (stock image)
* Server type and version: N/A
* Operating System and version: reproduced on Linux containers (host: macOS and Linux)
* Filters and plugins: `tail` (one with `multiline.parser`), `modify`, `rewrite_tag` / `in_emitter`, `stdout`, `null`

**Additional context**

We hit this migrating a production log pipeline: a low-volume app's multiline logs were silently missing ~10–27% of their timeout-flushed lines whenever a co-tailed high-volume input kept the shared emitter at its limit. Because the loss is reported as a successful emit (`emit_records_total` increments) and no drop metric fires, it is effectively undetectable from monitoring. We found it by random sampling our existing log pipeline and looking for those samples in the new Fluent Bit pipeline.

Suggested fix: in `process_record()` (`rewrite_tag.c:403–421`), handle `FLB_EMITTER_BUSY` explicitly — preserve the original record (or propagate the busy condition for retry) instead of falling through to `return FLB_TRUE`.

We are able to work around this issue by raising the memory buffer limit so the emitter practically never pauses.



Discussion:

Patch:
diff --git a/plugins/filter_rewrite_tag/rewrite_tag.c b/plugins/filter_rewrite_tag/rewrite_tag.c
index 080a096a4f7..3697fee4cce 100644
--- a/plugins/filter_rewrite_tag/rewrite_tag.c
+++ b/plugins/filter_rewrite_tag/rewrite_tag.c
@@ -414,7 +414,8 @@ static int process_record(const char *tag, int tag_len, msgpack_object map,
     /* Release the tag */
     flb_sds_destroy(out_tag);
 
-    if (ret == -1) {
+    if (ret < 0) {
+        *keep = FLB_TRUE;
         return FLB_FALSE;
     }
 
diff --git a/plugins/in_emitter/emitter.c b/plugins/in_emitter/emitter.c
index 2cdbccd09b2..2b80b9b920e 100644
--- a/plugins/in_emitter/emitter.c
+++ b/plugins/in_emitter/emitter.c
@@ -49,6 +49,7 @@ struct input_ref {
 
 struct flb_emitter {
     int coll_fd;                        /* collector id */
+    size_t pending_bytes;               /* bytes waiting in chunks list */
     struct mk_list chunks;              /* list of all pending chunks */
     struct flb_input_instance *ins;     /* input instance */
     struct flb_ring_buffer *msgs;       /* ring buffer for cross-thread messages */
@@ -82,14 +83,35 @@ struct em_chunk *em_chunk_create(const char *tag, int tag_len,
     return ec;
 }
 
-static void em_chunk_destroy(struct em_chunk *ec)
+static void em_chunk_destroy(struct flb_emitter *ctx, struct em_chunk *ec)
 {
+    if (ctx->pending_bytes >= ec->mp_sbuf.size) {
+        ctx->pending_bytes -= ec->mp_sbuf.size;
+    }
+    else {
+        ctx->pending_bytes = 0;
+    }
+
     mk_list_del(&ec->_head);
     flb_sds_destroy(ec->tag);
     msgpack_sbuffer_destroy(&ec->mp_sbuf);
     flb_free(ec);
 }
 
+static int is_queue_overlimit(struct flb_emitter *ctx, size_t append_size)
+{
+    if (ctx->ins->mem_buf_limit == 0) {
+        return FLB_FALSE;
+    }
+
+    if (ctx->ins->mem_chunks_size + ctx->pending_bytes + append_size >
+        ctx->ins->mem_buf_limit) {
+        return FLB_TRUE;
+    }
+
+    return FLB_FALSE;
+}
+
 int static do_in_emitter_add_record(struct em_chunk *ec,
                                     struct flb_input_instance *in)
 {
@@ -110,10 +132,10 @@ int static do_in_emitter_add_record(struct em_chunk *ec,
     if (ret == -1) {
         flb_plg_error(ctx->ins, "error registering chunk with tag: %s", ec->tag);
         /* Release the echunk */
-        em_chunk_destroy(ec);
+        em_chunk_destroy(ctx, ec);
         return -1;
     }
-    em_chunk_destroy(ec);
+    em_chunk_destroy(ctx, ec);
     return 0;
 }
 
@@ -131,6 +153,7 @@ int in_emitter_add_record(const char *tag, int tag_len,
     struct input_ref *i_ref;
     bool ref_found;
     struct mk_list *tmp;
+    int ret;
 
     struct em_chunk *ec;
     struct flb_emitter *ctx;
@@ -163,14 +186,15 @@ int in_emitter_add_record(const char *tag, int tag_len,
     }
 
 
-    /* Restricted by mem_buf_limit */
-    if (flb_input_buf_paused(ctx->ins) == FLB_TRUE) {
-        flb_plg_debug(ctx->ins, "emitter memory buffer limit reached. Not accepting record.");
-        return FLB_EMITTER_BUSY;
-    }
-
     /* Use the ring buffer first if it exists */
     if (ctx->msgs) {
+        /* Restricted by mem_buf_limit */
+        if (flb_input_buf_paused(ctx->ins) == FLB_TRUE) {
+            flb_plg_debug(ctx->ins,
+                          "emitter memory buffer limit reached. Not accepting record.");
+            return FLB_EMITTER_BUSY;
+        }
+
         memset(&temporary_chunk, 0, sizeof(struct em_chunk));
 
         temporary_chunk.tag = flb_sds_create_len(tag, tag_len);
@@ -190,6 +214,12 @@ int in_emitter_add_record(const char *tag, int tag_len,
                                      sizeof(struct em_chunk));
     }
 
+    if (is_queue_overlimit(ctx, buf_size) == FLB_TRUE) {
+        flb_plg_debug(ctx->ins,
+                      "emitter memory buffer limit reached. Not accepting record.");
+        return FLB_EMITTER_BUSY;
+    }
+
     /* Check if any target chunk already exists */
     mk_list_foreach(head, &ctx->chunks) {
         ec = mk_list_entry(head, struct em_chunk, _head);
@@ -210,8 +240,23 @@ int in_emitter_add_record(const char *tag, int tag_len,
         }
     }
 
+    /*
+     * When the emitter is paused, caller-side filters cannot ask the engine to
+     * retry the same record. Keep it in the emitter queue so the collector can
+     * ingest it after resume instead of forcing callers to drop or retag it.
+     */
+    if (flb_input_buf_paused(ctx->ins) == FLB_TRUE) {
+        flb_plg_debug(ctx->ins,
+                      "emitter memory buffer limit reached. Buffering record.");
+    }
+
     /* Append raw msgpack data */
-    msgpack_sbuffer_write(&ec->mp_sbuf, buf_data, buf_size);
+    ret = msgpack_sbuffer_write(&ec->mp_sbuf, buf_data, buf_size);
+    if (ret != 0) {
+        return -1;
+    }
+
+    ctx->pending_bytes += buf_size;
     return 0;
 }
 
@@ -411,8 +456,7 @@ static int cb_emitter_exit(void *data, struct flb_config *config)
 
     mk_list_foreach_safe(head, tmp, &ctx->chunks) {
         echunk = mk_list_entry(head, struct em_chunk, _head);
-        mk_list_del(&echunk->_head);
-        flb_free(echunk);
+        em_chunk_destroy(ctx, echunk);
     }
 
     if (ctx->msgs) {


Test patch:
diff --git a/tests/runtime/filter_rewrite_tag.c b/tests/runtime/filter_rewrite_tag.c
index 08cf37511a8..cef9ac38cc8 100644
--- a/tests/runtime/filter_rewrite_tag.c
+++ b/tests/runtime/filter_rewrite_tag.c
@@ -348,8 +348,10 @@ static void flb_test_heavy_input_pause_emitter()
         TEST_MSG("callback is not invoked");
     }
 
-    /* Input should be paused since Mem_Buf_Limit is small size.
-     * So got is less than heavy_loop.
+    /*
+     * Input should be paused since Mem_Buf_Limit is small size. Retagged
+     * records accepted within the emitter budget should drain, but the
+     * emitter must stop accepting before the full unbounded stream is queued.
      */
     if(!TEST_CHECK(heavy_loop > got)) {
         TEST_MSG("expect: %d got: %d", heavy_loop, got);
@@ -358,6 +360,61 @@ static void flb_test_heavy_input_pause_emitter()
     filter_test_destroy(ctx);
 }
 
+static void flb_test_busy_emitter_keeps_original()
+{
+    struct flb_lib_out_cb cb_data;
+    struct filter_test *ctx;
+    int ret;
+    int not_used = 0;
+    int bytes;
+    int heavy_loop = 100000;
+    int got;
+    char p[256];
+    int i;
+
+    cb_data.cb = cb_count_msgpack;
+    cb_data.data = &not_used;
+
+    ctx = filter_test_create((void *) &cb_data);
+    if (!ctx) {
+        exit(EXIT_FAILURE);
+    }
+    clear_output_num();
+
+    ret = flb_filter_set(ctx->flb, ctx->f_ffd,
+                         "Rule", "$key ^(rewrite)$ updated false",
+                         "Emitter_Mem_Buf_Limit", "1kb",
+                         NULL);
+    TEST_CHECK(ret == 0);
+
+    ret = flb_output_set(ctx->flb, ctx->o_ffd,
+                         "Match", "*",
+                         NULL);
+    TEST_CHECK(ret == 0);
+
+    ret = flb_service_set(ctx->flb, "Log_Level", "Off", NULL);
+    TEST_CHECK(ret == 0);
+
+    ret = flb_start(ctx->flb);
+    TEST_CHECK(ret == 0);
+
+    for (i = 0; i < heavy_loop; i++) {
+        memset(p, '\0', sizeof(p));
+        snprintf(p, sizeof(p), "[%d, {\"val\": \"%d\",\"key\": \"rewrite\"}]", i, i);
+        bytes = flb_lib_push(ctx->flb, ctx->i_ffd, p, strlen(p));
+        TEST_CHECK(bytes == strlen(p));
+    }
+
+    flb_time_msleep(1500);
+    got = get_output_num();
+
+    if (!TEST_CHECK(got == heavy_loop)) {
+        TEST_MSG("expect: %d got: %d", heavy_loop, got);
+    }
+
+    filter_test_destroy(ctx);
+}
+
 static void flb_test_issue_4793()
 {
     struct flb_lib_out_cb cb_data;
@@ -557,6 +614,7 @@ TEST_LIST = {
     {"not_matched",      flb_test_not_matched},
     {"keep_true",        flb_test_keep_true},
     {"heavy_input_pause_emitter", flb_test_heavy_input_pause_emitter},
+    {"busy_emitter_keeps_original", flb_test_busy_emitter_keeps_original},
     {"issue_4518", flb_test_issue_4518},
     {"issue_4793", flb_test_issue_4793},
     {"sigsegv_issue_5846", flb_test_issue_5846},