Everything for Logstash in one place — pick a section below. 13 reviewed items across 4 content types.
Quick Answer
Logstash separates ingestion, transformation, and delivery into distinct stages so each can use a rich, swappable plugin ecosystem independently — dozens of input types, dozens of output types — without tangling data acquisition with parsing logic. The filter stage, especially grok's regex-based parsing, is almost always the CPU bottleneck because pattern matching against unstructured text is expensive, while input and output are largely I/O-bound; when filters saturate CPU, events queue up behind them and total pipeline throughput drops to whatever the filters can chew through, regardless of how fast the input or output could otherwise go.
Detailed Answer
Think of a car wash with three stations in sequence: a pre-spray station that just gets cars wet, a scrub-and-dry station where people do the actual labor-intensive cleaning, and a final rinse station before cars roll out. If the scrub station has only one person working while cars arrive quickly, cars back up in the queue between spray and scrub — even though the spray and rinse stations are sitting completely idle, waiting for their turn. The bottleneck isn't spread evenly across the car wash; it's concentrated wherever the manual, expensive work happens.
Logstash was deliberately built with this three-stage separation — input plugins, filter plugins, output plugins — precisely so that acquisition, transformation, and delivery could each grow a rich, independently swappable plugin ecosystem (beats, kafka, syslog, and dozens more as inputs; elasticsearch, opensearch, s3, and dozens more as outputs) without those concerns being tangled into one monolithic script. It mirrors classic Unix pipeline philosophy: each stage does one job well, and data flows through a queue between stages rather than being handled by a single do-everything component.
Internally, an input plugin — say the beats input receiving from a Filebeat fleet shipping checkout-worker logs — hands raw events to Logstash's internal queue. A pool of filter worker threads (pipeline.workers) each pull a batch and run it through the configured filter chain in order: commonly grok (regex-based pattern extraction, notoriously CPU-expensive because it tries multiple pattern alternatives against every line), mutate (field renaming and type conversion, cheap), and date (timestamp parsing, moderate cost). Only after that chain completes does the transformed event reach an output plugin, which typically batches multiple events into one bulk API call for efficiency on the way out.
When the filter stage becomes the bottleneck in production, you see rising queue_push_duration metrics and workers pinned near 100% CPU, while the output plugin's own throughput — bulk requests per second against OpenSearch, say — stays flat or low, simply because it isn't being fed events fast enough to matter. The fix path is almost never 'add more workers' as a first move: it's reducing grok complexity (anchoring patterns with ^, avoiding greedy wildcards, ordering more specific patterns first so they can short-circuit), switching to dissect wherever the log format is fixed-position rather than genuinely needing regex flexibility, or scaling out horizontally with additional Logstash nodes behind a load balancer once the per-event cost itself is already reasonable.
The non-obvious gotcha: throwing more pipeline.workers at a filter bottleneck doesn't help if the grok patterns themselves are pathologically slow — a phenomenon resembling catastrophic backtracking in regex engines, where certain input shapes cause the pattern matcher to explore an exponential number of paths before matching or failing. You can end up with every worker thread pegged at 100% CPU, the JVM's garbage collector working harder under the memory churn, and adding more workers just means more threads simultaneously fighting for the same scarce CPU rather than actually increasing throughput. The real fix is almost always simplifying or replacing the expensive pattern, not adding parallelism around it.
Code Example
# logstash.conf - filter stage doing the expensive work between input and output
input {
beats {
port => 5044 # receives events shipped from Filebeat fleet
}
}
filter {
grok {
match => { "message" => "%{TIMESTAMP_ISO8601:ts} %{LOGLEVEL:level} %{GREEDYDATA:msg}" }
# expensive: regex alternation tried against every single line
}
# faster alternative for fixed-format lines, no backtracking risk:
# dissect { mapping => { "message" => "%{ts} %{level} %{msg}" } }
}
output {
elasticsearch {
hosts => ["opensearch.internal:9200"]
index => "checkout-worker-logs-%{+YYYY.MM.dd}"
}
}
# pipelines.yml - scale worker count to match filter CPU cost, not input volume alone
- pipeline.id: checkout-worker-logs
pipeline.workers: 6Interview Tip
A junior engineer typically answers 'Logstash has three stages: input, filter, output,' which just names the architecture without explaining why it matters operationally. For a senior/architect role, the interviewer is actually looking for you to identify why filters — specifically grok — are the CPU bottleneck in nearly every real pipeline, and what you'd actually do about it: replacing grok with dissect where possible, anchoring and ordering patterns for early-exit matching, or scaling horizontally, rather than reflexively cranking up pipeline.workers, which doesn't fix a fundamentally slow pattern and can worsen CPU contention instead.
◈ Architecture Diagram
┌───────┐ ┌─────────────┐ ┌────────┐ │ Input │ → │ Filters │ → │ Output │ │(I/O) │ │ grok=slow ● │ │ (I/O) │ └───────┘ └─────────────┘ └────────┘ idle bottleneck idle (fast) (backlog builds) (fast)
💬 Comments
Quick Answer
Use the monitoring API (GET _node/stats/pipelines) to confirm which pipeline and filter stage is consuming CPU and taking abnormally long per event, then isolate the specific pattern via the Grok Debugger or by temporarily disabling suspect patterns one at a time, looking for a greedy, unanchored pattern like GREEDYDATA combined with an alternation that can match the same input in many overlapping ways. The fix is almost always rewriting the pattern to be more specific and anchored, or replacing grok with dissect for fixed-format logs — not adding more workers or CPU.
Detailed Answer
Imagine an overzealous proofreader given one sloppy instruction: check every possible way this sentence could be parsed before deciding what it means. For a short, clear sentence, that's instant. But for a long, ambiguous paragraph full of repeated words and delimiters, the number of 'possible ways to read it' the proofreader tries to consider can explode combinatorially, and they get stuck rereading the same paragraph for minutes, burning all their attention on one sentence while a stack of other work piles up behind them, untouched.
That's what happens inside Logstash's grok filter, built on the Oniguruma and Joni regex engines, when a pattern is too permissive. Grok patterns are named regex shortcuts, like %{IP} or %{GREEDYDATA}, composed together, designed to make regex-based log parsing approachable without hand-writing raw regular expressions. But grok doesn't protect you from writing a combination of patterns that triggers catastrophic backtracking, where the regex engine, faced with ambiguous or nested repetition, tries an exponential number of ways to match before giving up or succeeding on a single line.
The diagnostic sequence starts with the monitoring API: GET _node/stats/pipelines/<name> shows which pipeline's filter stage has abnormally high duration_in_millis per event, correlated with top -H showing Logstash's JVM threads pegged at CPU. From there, isolate the specific pattern by testing candidate log lines against each configured grok pattern individually, using the Grok Debugger or by temporarily commenting out patterns and watching for _grokparsefailure tags disappearing or CPU dropping — looking specifically for combinations of GREEDYDATA or .* sitting next to other loosely-bounded patterns, especially where the log line contains repeated delimiter characters that create many equally-valid ways to split the string.
In production this shows up as every pipeline.workers thread pinned at 100% CPU, events.duration_in_millis climbing while events.out throughput craters, and the persistent queue (if configured) filling because filters can't drain it fast enough — for a payments-api log pipeline this cascades quickly since transaction logs are both high-volume and time-sensitive. Ongoing prevention means routing any new grok pattern through a staging pipeline fed with representative production log volume before it ever reaches the main pipeline, and preferring dissect over grok wherever the log format has a fixed, predictable structure, since dissect does simple string splitting with zero backtracking risk.
The non-obvious gotcha: a grok pattern that has run fine for months can suddenly start pegging CPU not because the pattern itself changed, but because an upstream application changed its log format slightly — checkout-worker might start emitting a longer, more repetitive error message inside a field that a GREEDYDATA pattern was matching — and the same 'safe' pattern that worked fine on short messages now triggers exponential backtracking on the new, longer ones. Grok performance is never purely a property of the pattern in isolation; it depends on the actual data shape flowing through it, which is why any pattern audit needs representative production samples, not unit-test-style short strings that never expose the edge case.
Code Example
# check which pipeline and stage is CPU-bound
curl -s localhost:9600/_node/stats/pipelines?pretty | jq '.pipelines["payments-api-logs"].plugins.filters'
# isolate the pathological pattern with the grok debugger CLI equivalent
# pattern suspected of catastrophic backtracking:
# match => { "message" => "%{GREEDYDATA}%{GREEDYDATA:err}%{GREEDYDATA}" }
# safer, anchored replacement pattern for the same field
filter {
grok {
match => { "message" => "^%{TIMESTAMP_ISO8601:ts} %{LOGLEVEL:level}: %{GREEDYDATA:err}$" }
}
}
# or replace with dissect entirely for a fixed-delimiter log format
filter {
dissect {
mapping => { "message" => "%{ts} %{level}: %{err}" }
}
}Interview Tip
A junior engineer typically answers 'the regex is too complex, simplify it,' which is true but vague and doesn't demonstrate a real diagnostic process. For a senior/architect role, the interviewer is actually looking for you to name the actual mechanism — catastrophic backtracking from greedy, overlapping pattern alternatives — and describe a concrete diagnostic path through the monitoring API and Grok Debugger rather than guessing at the fix. A strong signal is recognizing that a previously-fine pattern can start misbehaving purely because upstream log content changed shape, meaning ongoing grok review needs to happen against real production samples, not a one-time test performed during initial setup.
◈ Architecture Diagram
┌────────┐ ┌───────────────┐ ┌────────┐
│ Input │ → │ grok (100%CPU)│ → │ Output │
└────────┘ └───────────────┘ └────────┘
│
backtracking explosion
(events.duration_in_millis ↑)💬 Comments
Quick Answer
Watch the gap between events.in and events.out rates per pipeline — a growing, sustained gap means events are accumulating somewhere even while the process stays up — alongside persistent queue size relative to its configured max, and per-plugin duration_in_millis to spot which stage is slow. A sudden, sustained divergence between in-rate and out-rate is the earliest reliable signal of an emerging backlog, well before queue capacity is exhausted or the output starts actively rejecting writes.
Detailed Answer
Picture a bathtub with the faucet running and the drain partially clogged. The water level doesn't jump instantly — it creeps up gradually as inflow slightly exceeds outflow, and if you're only glancing at the tub occasionally, you might not notice the slow rise until it's nearly overflowing. But if you're watching the actual flow rates, how much water is coming in per minute versus draining out per minute, you'd catch the mismatch within the first few minutes, long before the water level itself becomes visually alarming.
Logstash exposes exactly these flow rates through its monitoring API and the _node/stats endpoint, because Elastic designed the pipeline to be introspectable at each stage specifically so operators don't have to wait for a hard failure — queue full, disk full, process OOM — to know something is wrong. The core insight baked into this design is that a pipeline can look perfectly healthy by a simple up/down check while quietly losing the race between ingestion rate and processing rate for many minutes before anything actually crashes.
Internally, each pipeline tracks cumulative events.in, events.filtered, and events.out counters, plus per-plugin timing where duration_in_millis divided by event count gives the average per-event processing time for each input, filter, and output plugin. Sampling these counters every 30 to 60 seconds and computing rates gives you in-rate versus out-rate directly; if in-rate consistently exceeds out-rate, the difference is accumulating somewhere — either in the persistent queue on disk, the in-memory queue, or, worst case with a memory queue and no backpressure protection configured, getting silently dropped once buffers overflow.
In production, dashboards typically graph events.in per second against events.out per second per pipeline side by side, persistent queue disk usage as a percentage of queue.max_bytes, and per-filter average duration on the same timeline. Alerting rules page when in-rate exceeds out-rate by more than some margin, say 10 percent, sustained for five or more minutes, since that's a genuine leading indicator — versus ticketing on slower creeping trends like gradually increasing per-event filter duration, which is often an early sign of a grok pattern becoming pathological as upstream data shape drifts.
The non-obvious gotcha: events.out counts events leaving Logstash's own filter and output stage — it does not necessarily mean the output destination actually persisted them successfully, especially with output configurations that don't enforce strict acknowledgment on every write. A healthy-looking in-rate to out-rate balance inside Logstash can coexist with a real data-loss problem happening one hop further downstream at the storage layer, which is why pipeline metrics need to be paired with destination-side ingestion metrics, like OpenSearch's own indexing rate and rejected-document count, to get the complete picture rather than a partial one that looks reassuring in isolation.
Code Example
# poll pipeline in/out rates every 30s and compute the delta
while true; do
curl -s localhost:9600/_node/stats/pipelines?pretty \
| jq '.pipelines["payments-api-logs"].events | {in, out, filtered}'
sleep 30
done
# check persistent queue usage against its configured cap
curl -s localhost:9600/_node/stats/pipelines?pretty \
| jq '.pipelines["payments-api-logs"].queue'Interview Tip
A junior engineer typically answers 'alert if Logstash is down,' missing that real outages are usually preceded by a slow-building backlog while the process stays up the entire time. For a senior/architect role, the interviewer is actually looking for you to reason about leading versus lagging indicators — the in-rate to out-rate divergence and queue utilization trend as leading signals, versus a full queue or crashed process as lagging, too-late signals — and to recognize that Logstash's own events.out metric doesn't guarantee the downstream store actually accepted the data, so full observability requires correlating both sides of the pipe, not just the Logstash side.
◈ Architecture Diagram
┌────────┐ events.in ↑↑ │ Input │ ──────────────────┐ └────────┘ │ gap growing = ┌────────┐ │ backlog forming │ Output │ ←─────────────────┘ └────────┘ events.out (flat)
💬 Comments
Quick Answer
Validate syntax offline with bin/logstash --config.test_and_exit, then run the candidate config against a recorded sample of real production events in an isolated test pipeline before touching the live one, and rely on Logstash's config.reload.automatic behavior, which falls back to the last-known-good compiled pipeline if a reload fails rather than crashing the running process. Always deploy the config file via an atomic rename rather than an in-place edit, since a reload can otherwise pick up a half-written, transiently invalid file.
Detailed Answer
Think about a surgeon who wants to try a new suturing technique. They don't test it for the first time on a live patient in the operating room — they practice on a synthetic model or a cadaver first, confirm the technique works and doesn't cause unexpected damage, and only then apply it to a real patient, usually starting with lower-risk cases before it becomes their default approach for everything they do.
Logstash config changes deserve the same discipline, because a malformed or subtly wrong filter doesn't just fail to start cleanly — depending on the specific mistake, it can either fail fast, which is the good outcome, or silently misprocess or drop events across an entire pipeline serving multiple services at once. Elastic built --config.test_and_exit specifically as an offline syntax and structural check, separate from actually running the pipeline, precisely so teams have a fast, side-effect-free way to catch obvious mistakes — unbalanced braces, invalid plugin options, unknown filter names — before touching anything live.
The safe rollout path: first run bin/logstash --config.test_and_exit -f new_pipeline.conf to catch syntax errors. Because syntax validity doesn't guarantee correct behavior, next run the candidate config in an isolated test pipeline, defined as a second entry in pipelines.yml, fed with a recorded sample of real production events — a file input replaying a saved batch of actual checkout-worker or user-auth-service log lines — and inspect the resulting output events for correct field extraction. Only after confirming expected output do you deploy to the actual production pipeline definition, ideally with config.reload.automatic: true, so Logstash picks up the new file, and critically, if the new config fails to compile at reload time, Logstash logs the error and continues running the OLD compiled pipeline rather than crashing — this fail-safe-to-old-config behavior is a major reason to prefer reload over a full process restart for routine config changes.
In production, you monitor the Logstash log for Reloading and Failed to reload messages immediately after any deploy, watch events.out rate before and after the change to confirm the new config isn't silently dropping or misrouting events, and keep the previous config version easily accessible in git, tagged by deploy, for instant rollback by re-triggering a reload with the reverted file. Teams that skip the isolated test-pipeline step often discover mapping or type mismatches only after the new config has already been indexing malformed data into OpenSearch for hours, which is a much more expensive fix than catching it beforehand.
The non-obvious gotcha: config.reload.automatic watches for file changes but does so on a debounce or polling interval, checking every few seconds by default, and if your deployment tooling writes the new config in a way that produces a transient, invalid intermediate state — an in-place multi-line edit rather than an atomic rename — Logstash can pick up a half-written config file mid-write and reload with a genuinely broken pipeline before your deployment tool has even finished writing the complete file. Always deploy config via atomic file replacement, writing to a temp file and then renaming it into place, never by streaming an in-place edit to the live config path.
Code Example
# 1. offline syntax check, no side effects bin/logstash --config.test_and_exit -f /etc/logstash/conf.d/payments-api.conf # 2. dry run against recorded production events in an isolated test pipeline # pipelines.yml # - pipeline.id: payments-api-canary # path.config: "/etc/logstash/conf.d/payments-api-canary.conf" # 3. deploy via atomic rename, never in-place edit, so reload never sees a half-written file cp new_payments-api.conf /etc/logstash/conf.d/payments-api.conf.tmp mv /etc/logstash/conf.d/payments-api.conf.tmp /etc/logstash/conf.d/payments-api.conf # 4. confirm the reload succeeded, not just that the file changed tail -f /var/log/logstash/logstash-plain.log | grep -i reload
Interview Tip
A junior engineer typically answers 'just run --config.test_and_exit before deploying,' which only catches syntax errors, not logical ones, like a grok pattern that compiles fine but extracts the wrong fields entirely. For a senior/architect role, the interviewer is actually looking for a full staged rollout: offline syntax check, a dry run against real recorded production data in an isolated pipeline, atomic file replacement to avoid a reload picking up a half-written config, and an explicit, version-controlled rollback plan. They're testing whether you treat pipeline configuration like the production code it effectively is, not a one-off text file.
◈ Architecture Diagram
┌─────────┐ ┌──────────┐ ┌─────────┐ ┌────────┐ │ Syntax │→ │ Canary │→ │ Atomic │→ │Monitor │ │ Test │ │ Pipeline │ │ Deploy │ │Reload │ └─────────┘ └──────────┘ └─────────┘ └────────┘ ✓ fast fail ✓ real data ✓ no half-write ✓ rollback ready
💬 Comments
Quick Answer
Enable the persistent (disk-backed) queue so events accepted from an input survive a JVM crash before the output has written them, size pipeline.workers and pipeline.batch.size to match actual filter CPU cost rather than defaults, and route unparseable events to a dead letter queue instead of blocking or silently dropping them. The core principle is decoupling ingestion rate from processing/output rate through durable buffering, so a slow downstream store causes backpressure, not silent loss.
Detailed Answer
Picture a busy restaurant kitchen during a dinner rush, with a physical order rail between the servers and the line cooks. Orders get clipped onto that rail the moment a server submits them, and they stay there, visible and durable, until a cook actually plates the dish. If the grill station gets backed up, tickets pile up on the rail instead of vanishing — the rail is a strip of steel, not a whiteboard someone could accidentally wipe. That durability is exactly the property you want between the moment data enters a pipeline and the moment it's truly, safely delivered.
Logstash's persistent queue is that durable rail. By default, Logstash buffers events in memory between the input and filter/output stages, which is fast but evaporates completely on a crash or forced restart — anything accepted from an input but not yet handed off to the output is simply gone. Elastic added queue.type: persisted specifically for pipelines where that's unacceptable, such as billing or audit trails from a service like payments-api: accepted events are written to disk and acknowledged back to the input only after that write succeeds, so a JVM crash mid-processing doesn't silently drop in-flight data.
Internally, the flow is input, then the persistent queue (disk-backed, acknowledging writes), then a pool of filter worker threads (pipeline.workers) each pulling a batch of pipeline.batch.size events through the configured filter chain (grok, mutate, dissect), then an output plugin. Events that fail every filter match aren't silently discarded by default configuration choices you make — routing them to the dead letter queue (dlq.enable: true) preserves them for later replay once you've fixed whatever broke parsing, rather than losing them or blocking the whole batch behind one bad event. Backpressure flows naturally end to end: if the output is slow or rejecting writes, workers block trying to write results, the persistent queue fills toward queue.max_bytes, and that in turn slows how fast the input reads new data — the pipeline self-regulates instead of buffering unboundedly.
At scale, the metrics worth watching are queue.events (and its percentage of max_bytes, since a queue approaching its cap means the output genuinely can't keep up and needs scaling or fixing), pipeline worker CPU (workers pegged near 100% doing grok usually means a regex pattern is too expensive), and dead letter queue growth (a growing DLQ typically means an upstream schema change broke a grok pattern and needs active draining, not just ignoring). A persistent queue sized too small for a real burst just moves the failure earlier without fixing it.
The gotcha that catches experienced engineers: the persistent queue guarantees events aren't lost between the input and being handed to the output — it does not guarantee the output itself succeeded. If Elasticsearch or OpenSearch returns a partial bulk failure (some documents indexed, others rejected for a mapping conflict), Logstash's default handling can still acknowledge the whole batch off the queue unless you've explicitly configured retry-on-failure and DLQ routing for output-level errors specifically. 'I enabled the persistent queue so I can't lose data' is a dangerously incomplete assumption — it protects one hop of the journey, not the whole trip.
Code Example
# logstash.yml - queue durability settings, applied per pipeline queue.type: persisted # disk-backed queue instead of in-memory queue.max_bytes: 4gb # cap so disk can't fill unbounded during an outage path.queue: /var/lib/logstash/queue dead_letter_queue.enable: true # unparseable events go here instead of vanishing path.dead_letter_queue: /var/lib/logstash/dlq # pipelines.yml - tune workers/batch size for the payments-api-logs pipeline - pipeline.id: payments-api-logs pipeline.workers: 8 # match to available CPU cores doing grok parsing pipeline.batch.size: 250 # events pulled per worker per batch path.config: "/etc/logstash/conf.d/payments-api.conf" # inspect DLQ growth to catch silent parse failures ls -la /var/lib/logstash/dlq/payments-api-logs/
Interview Tip
A junior engineer typically answers 'enable the persistent queue and you won't lose data,' and stops there. For a senior/architect role, the interviewer is actually looking for you to trace the full backpressure chain — how a slow output propagates back through workers to the queue to the input — and, critically, to recognize the gap between 'accepted onto the queue' and 'successfully written by the output,' since partial bulk failures at the output layer aren't covered by the persistent queue guarantee alone. They want to hear queue durability described as one layer of a multi-layer reliability story, not a silver bullet that makes the whole pipeline lossless by itself.
◈ Architecture Diagram
┌───────┐ ┌──────────────┐ ┌─────────┐ ┌────────┐
│ Input │ → │Persist. Queue│ → │ Filters │ → │ Output │
└───────┘ │ (disk, acked)│ └─────────┘ └────────┘
└──────────────┘ │
partial fail?
→ DLQ (not lost)💬 Comments
Context
A payments company whose Logstash layer (between Beats and Elasticsearch) ran with default in-memory queues — discovered to be dropping events whenever Elasticsearch had indexing pressure or Logstash restarted, during exactly the incidents when logs mattered most.
Problem
A post-incident review couldn't reconstruct a timeline because the log gap coincided with the outage (backpressure dropped events); compliance flagged the loss; and mapping-error events (400s from ES) were vanishing silently — an estimated 0.3% of all events, invisibly.
Solution
Re-architected for durability: persistent queues enabled per pipeline (disk-backed, sized for 2 hours of peak ingest) so backpressure and restarts buffer instead of drop; dead-letter queues enabled for the elasticsearch output so mapping-rejected events land in the DLQ instead of the void; a dedicated DLQ-processing pipeline retries transient classes and routes hard failures to an S3 bucket with a weekly triage report ranking rejection causes by count. Queue depth and DLQ rate became dashboards with alerts (queue >60% for 10m pages; DLQ rate >0.1% tickets).
Commands
pipelines.yml: {queue.type: persisted, queue.max_bytes: 8gb, path.dead_letter_queue: /var/lib/logstash/dlq}dlq pipeline: input {dead_letter_queue {...}} -> classify -> retry | s3 archivemonitor: GET _node/stats/pipelines | jq '.pipelines[].queue.events_count'
Outcome
Event loss during the next Elasticsearch degradation: zero (queue absorbed 40 minutes of backpressure); the DLQ triage surfaced a date-parsing bug in one service that had been silently discarding 200K events/week since forever; compliance closed the finding.
Lessons Learned
The DLQ's weekly triage report was the surprise value — silent 400s from Elasticsearch had been hiding real application bugs. Persistent queue sizing needs peak-rate math plus headroom; the first sizing (30 minutes) proved too small in the very first real incident.
💬 Comments
Context
One Logstash config file grown over four years: every input (Beats, Kafka, syslog, HTTP), a 700-line filter maze of nested conditionals, and five outputs — where any change risked everything and one slow output backpressured all sources.
Problem
The syslog input starved whenever the S3 archive output slowed (shared pipeline, shared backpressure); config changes required full restarts affecting all flows; testing a filter change meant praying; and CPU profiling was useless because everything shared one pipeline's workers.
Solution
Decomposed via multiple pipelines with pipeline-to-pipeline communication: per-source ingest pipelines (beats, kafka, syslog) doing protocol handling only -> a normalize pipeline owning the shared ECS mapping -> per-destination output pipelines (es-hot, s3-archive, siem) each with its own queue and worker settings. The distributor pattern routes by data class. Now the S3 output's slowness fills only its own queue; syslog flows untouched. Each pipeline gets tuned workers/batch sizes; config changes reload per pipeline; and pipeline-level monitoring finally attributes CPU and lag to a stage.
Commands
pipelines.yml: [{pipeline.id: ingest-syslog, ...}, {pipeline.id: normalize, ...}, {pipeline.id: out-s3, queue.type: persisted}]output {pipeline {send_to => ['normalize']}} # p2p wiringGET _node/stats/pipelines?pretty # per-stage events, queue depth, worker utilization
Outcome
The cross-contamination outage class ended (slow S3 no longer touches syslog); filter changes deploy per-stage with scoped blast radius; per-pipeline stats cut a chronic CPU mystery to a one-hour diagnosis (the SIEM output's TLS renegotiation). Restart-required changes dropped ~90%.
Lessons Learned
The normalize stage owning ECS mapping in exactly one place eliminated the field-drift between destinations that had plagued the monolith. Internal pipeline queues need sizing too — the first version left defaults and moved the backpressure problem one hop upstream.
💬 Comments
Symptom
Log ingestion appears to stop or fall far behind in near-real-time; upstream inputs (Kafka, Beats, file inputs) show growing lag or connection backlogs, but Logstash itself doesn't crash or log an obvious fatal error.
Error Message
429 Too Many Requests or 'Retrying failed action' repeating in Logstash's Elasticsearch output logs, with pipeline events.duration_in_millis climbing
Root Cause
When an output destination (commonly Elasticsearch/OpenSearch) is slow, rejecting writes (e.g. returning 429s under its own load, or hitting its own disk watermark), Logstash's queue fills up and backpressures the entire pipeline — inputs stop pulling new events because there's nowhere to put them — even though nothing in Logstash itself has failed. Because this shows up as 'ingestion slowed down' rather than a clear error, it's easy to mistake for an input-side or network problem when the actual bottleneck is downstream.
Diagnosis Steps
Solution
Identify and resolve the output-side bottleneck first (Elasticsearch/OpenSearch capacity, its own disk watermark, index rollover/ILM lag), since fixing Logstash configuration without addressing the actual downstream constraint won't help; once the output recovers, the backlog will drain as the pipeline catches up, though very large backlogs may need throughput tuning (batch size, worker count) to drain faster.
Commands
curl -s localhost:9600/_node/stats/pipelines?pretty | jq '.pipelines.main.plugins.outputs'
GET _cat/thread_pool/write?v
GET _nodes/stats/thread_pool
Prevention
Monitor per-plugin worker_utilization and output error rates as first-class pipeline health signals, not just input lag, since output-caused backpressure looks identical to an input problem from the input side alone. Size persistent queues and monitor 'queue full' events so backpressure is visible before it fully stalls ingestion.
💬 Comments
Symptom
A subset of log lines never make it to the destination index (missing data/alerts) or the pipeline's overall throughput drops sharply after a new log format or grok pattern change, without a corresponding fatal pipeline error.
Error Message
_grokparsefailure tag appearing on affected events, or high events.duration_in_millis attributed specifically to the grok filter stage
Root Cause
grok is regex-based and can be CPU-expensive, especially with unanchored or catastrophically backtracking patterns; on malformed or unexpected input that doesn't cleanly match, a poorly written pattern can both consume disproportionate CPU per event (slowing the whole pipeline) and tag events with _grokparsefailure, which — if a later stage of the pipeline conditionally drops failed-parse events — results in silently missing data rather than a visible error.
Diagnosis Steps
Solution
Identify the specific grok pattern causing the slowdown/failures via per-filter timing (events.duration_in_millis broken down per filter), fix or replace the pattern (anchor it properly, or switch to dissect for fixed-delimiter formats which is far cheaper than grok), and audit whether _grokparsefailure-tagged events are being silently dropped downstream versus routed somewhere visible.
Commands
curl -s localhost:9600/_node/stats/pipelines?pretty | jq '.pipelines.main.plugins.filters'
bin/logstash --config.test_and_exit -f pipeline.conf
grep _grokparsefailure output.log | wc -l
Prevention
Prefer dissect over grok for any log format with a fixed, predictable delimiter structure, reserving grok for genuinely unstructured text. Gate grok filters with conditionals so they only run against message types that actually need them, rather than running against every event. Route _grokparsefailure-tagged events to a visible dead-letter index rather than silently dropping them.
💬 Comments
Symptom
Logstash throughput collapses from 40K to 2K events/sec within minutes of an upstream service deploy; all pipeline workers pin CPU; queues fill; downstream dashboards go stale. No Logstash change shipped. Restarting helps for seconds, then re-pins.
Error Message
No error — that's the problem. Hot-thread dump shows every worker thread in java.util.regex.Pattern matcher loops inside the same grok filter for minutes per event.
Root Cause
An upstream deploy changed a log format subtly (a field that used to be single-token began containing spaces). The pipeline's grok pattern — full of greedy .* segments and optional groups without anchors — didn't fail on the new lines; it catastrophically backtracked, spending minutes of regex-engine time per event before eventually matching or failing. A pattern that had 'worked for years' was one input-shape change away from quadratic behavior, and no timeout bounded it.
Diagnosis Steps
Solution
Immediate mitigation: routed the offending service's stream around the grok stage (tag-and-skip) to restore pipeline health, then rewrote the pattern — anchored (^...$), greedy wildcards replaced with explicit character classes and dissect for the fixed-position prefix (dissect is non-backtracking and ~10x cheaper for structured-ish lines), grok kept only for the truly variable tail. Set a grok timeout_millis as the global backstop so any future pathological match fails the event into _groktimeout tagging instead of eating the worker.
Commands
GET _node/hot_threads?threads=10
filter {grok {match => {...} timeout_millis => 500 tag_on_timeout => '_groktimeout'}}bench: bin/logstash -e 'config' < adversarial_samples.log --pipeline.workers 1
Prevention
Every grok pattern gets: anchors, a timeout, and a benchmark test with adversarial inputs (including 'what if this field grows spaces') in CI. Prefer dissect for fixed-structure prefixes. Monitor per-filter time via pipeline stats — grok cost regressions show there before they page.
💬 Comments