Skip to main content
Data Engineering

Streaming architecture in practice: when real-time earns its complexity

Most systems that stream did not need to. Some absolutely did, and a scheduled job would have failed the mission. This is how we tell the two apart, what the decisions cost when they turn out wrong, and what a healthy streaming system looks like on the dashboard.

What a streaming architecture actually buys

Streaming buys one thing a batch system cannot: a short interval between an event happening and something acting on it. Scale, decoupling, and replay get cited as reasons to stream, and all three are available from batch designs too, usually for less money and with fewer parts to operate. So the opening question on a streaming design is never which broker. It is how small that interval has to be, who is waiting on it, and what specifically goes wrong when it is larger.

That question has a testable answer, and the answer is usually written down somewhere already. If a person reads a dashboard once each morning, the requirement is hours and a scheduled job is the correct engineering. If an authorization hold has to be placed before a payment settles, the requirement is a few hundred milliseconds and no batch design meets it. Most real requirements land between those poles, and the middle is exactly where teams overbuild.

Our engineers build both shapes across federal, state, and commercial work, and the split in our own delivery runs closer to batch than most teams expect. Streaming is right often enough that we keep it as a core skill, and rare enough that reaching for it by default is a reliable way to spend a program's budget on operations instead of outcomes.

Workload fit for a continuous streaming design

Holds and blocks on live transactions
94%
Equipment telemetry with alarm thresholds
90%
Change data capture into a serving index
86%
Continuously refreshed operational displays
79%
Online feature computation for model scoring
76%
Scheduled reporting and regulatory extracts
64%

Editorial weighting from practitioner reading of public architecture literature. Illustrative, not a measured statistic.

The cadence test, run before any tool selection

Name the consumer of the output. Name the decision that consumer makes. Then name the cost of that decision being made on data that is five minutes stale, then an hour stale, then a day stale. Three numbers, one page. If the cost curve is flat between five minutes and an hour, streaming is not the requirement and a short-interval scheduled job will serve the same decision at a fraction of the operating load.

The curve bends sharply in a few identifiable places. It bends when a human or machine action is irreversible once taken, which is why payment holds, safety interlocks, and dispatch routing stream. It bends when the data has a short useful life, as with equipment vibration signatures that only matter before a bearing fails. It bends when a downstream index has to agree with a source system continuously, which is the change-data-capture case. Outside those, the bend is usually imagined.

Three shapes, and what each one costs to own

The choice is not binary. Micro-batch sits between the poles and covers more requirements than either neighbor, at meaningfully lower operating cost than continuous streaming.

PropertyScheduled batchMicro-batch (1–15 min)Continuous streaming
End-to-end intervalHours to a daySingle-digit minutesMilliseconds to seconds
Cost when idleNear zero; compute bills only while the job runsLow; short bursts on a scheduleContinuous; brokers and consumers bill around the clock
Fixing a logic bugRe-run the job for the affected windowRe-run the affected intervalsReplay from the retained log, if retention was long enough
State handlingStateless by construction; each run reads the sourceMostly stateless with a high-water markDistributed keyed state that must be checkpointed and bounded
Failure blast radiusOne late reportOne late intervalGrowing consumer lag that compounds until drained
Team skill requiredSQL and a schedulerSQL, a scheduler, watermark disciplinePartitioning, delivery semantics, state backends, on-call

The five decisions that are expensive to reverse

Everything in a streaming system is cheap to change except five things. Getting these right at design time is most of the value an experienced team brings, because each one is embedded in every producer and consumer written afterward.

  • The partition key. It fixes both ordering and parallelism. Ordering in Apache Kafka is guaranteed within a partition, never across them, so the key decides which events are ordered relative to each other. Changing it later re-shuffles history and breaks every stateful consumer keyed on the old scheme.
  • The event schema and its compatibility mode. Backward, forward, or full compatibility in the schema registry governs which future changes are legal. A schema published without required-field discipline will be produced against by a dozen services within a quarter.
  • Delivery semantics. At-most-once, at-least-once, or effectively-once. This choice propagates into every sink, because idempotency has to be implemented at the write, not promised by the framework.
  • Whether the log is the system of record. If the log is authoritative, retention becomes a data-durability question rather than a buffer-sizing question, and the compliance posture changes with it.
  • Event time versus processing time. The semantics of every window, every aggregate, and every late-arrival rule follow from this. Restating a year of aggregates because the original design windowed on arrival time is a project, not a patch.

Delivery semantics, stated plainly

Exactly-once is the most oversold phrase in this field. What Kafka and Apache Flink implement is effectively-once processing: an idempotent producer plus a transactional protocol that makes the visible result equivalent to a single application of each record, within the boundary of the systems participating in the transaction. Kafka has supported idempotent producers and transactions since the 0.11 release in 2017, and Flink pairs its checkpoint barriers with a two-phase commit on the sink.

The boundary is the part that gets skipped. If the sink is an external database, a search index, or a REST endpoint that does not participate in the transaction, the guarantee stops at the edge of the framework. The practical rule our team applies: build for at-least-once delivery and make every write idempotent by natural key, then add transactional sinks only where duplicate application is genuinely unrecoverable. That design survives replay, survives operator restarts, and survives the day someone reprocesses a topic by hand.

Exactly-once is the most oversold phrase in this field. Build for at-least-once and make every write idempotent by natural key.

Event time, watermarks, and the late data problem

Events arrive out of order. A mobile device buffers offline and uploads an hour of readings at once. A partition lags and its records land after a peer's. Any system that aggregates over time has to decide when a window is complete, and a watermark is the mechanism: an assertion that no event older than timestamp T will still arrive.

That assertion is always wrong sometimes. The design question is what happens when it is. Flink lets a window keep an allowed-lateness period after the watermark passes, and route anything later than that to a side output. Teams that skip the side output are silently dropping data, and they usually find out during an audit rather than on a dashboard. Every windowed pipeline we build emits a late-record counter and a watermark-skew gauge, because a watermark drifting away from wall-clock time is the earliest warning that a source has stalled.

What good means numerically

Streaming systems fail quietly, so the acceptance criteria have to be numbers rather than adjectives. Six that we hold ourselves to on delivery:

Consumer lag measured in seconds, not records. Record counts are meaningless across topics with different rates. Lag in time is comparable, alertable, and understandable by a program manager. A healthy pipeline holds p99 lag under a stated ceiling, commonly 30 to 60 seconds for operational displays.

End-to-end latency measured by a canary. Inject a synthetic heartbeat event at the producer, timestamp it at the sink, and publish the difference. Broker-side metrics measure the broker. Only a canary measures what the customer experiences.

Sustained headroom of 40 percent or better. Run steady-state at no more than 60 percent of measured per-partition capacity. The remaining margin is not waste, it is recovery budget.

A stated drain time after an outage. This is arithmetic, not a guess. If arrivals continue at rate λ during an outage of duration T, the backlog is λT. If the pipeline can process only 1.2λ once it recovers, the surplus draining that backlog is 0.2λ, so drain time is five times the outage. A one-hour outage becomes a five-hour recovery. Sizing for 2λ turns the same outage into a one-hour recovery.

Checkpoint duration below checkpoint interval. When a stateful job's checkpoint takes longer than the gap between checkpoints, the job is already in trouble and the next failure will be expensive.

A bounded and observed dead-letter queue. Depth, age of oldest message, and a named owner. A dead-letter queue nobody reads is a data-loss mechanism with extra steps.

The failure modes teams actually hit

Key skew. One tenant, one device, or one null-valued key routes a disproportionate share of traffic to a single partition. Parallelism stops helping because Kafka consumers in a group cannot exceed the partition count, so extra consumers sit idle while the hot partition falls behind. The fix is a composite or salted key, chosen before launch.

Unbounded state. A keyed aggregation with no time-to-live grows until the state backend exhausts disk. This surfaces months after go-live, which is why state TTL belongs in the first version rather than the hardening sprint.

Rebalance storms. A consumer that pauses longer than max.poll.interval.ms, which defaults to 300000 milliseconds, gets evicted from the group. Its partitions redistribute, the replacement inherits the same slow work, and the group thrashes. Long per-record processing belongs behind an async boundary, not inside the poll loop.

Poison records with no exit. One malformed message at the head of a partition blocks everything behind it forever. Deserialization failures need a route to the dead-letter queue on the first attempt, with the raw bytes preserved for diagnosis.

Replay that double-counts. The team reprocesses a topic to fix a bug and the downstream totals double, because the sink was append-only rather than keyed upsert. This is the idempotency rule failing in production instead of design review.

Monitoring the wrong layer. Broker CPU can be flat while lag climbs steadily. Alert on lag, watermark skew, checkpoint failures, and dead-letter depth. Infrastructure metrics are for diagnosis after the alert, not for the alert itself.

Where the money goes

Streaming cost is dominated by capacity that runs whether or not data flows. An Amazon Kinesis Data Streams shard in provisioned mode bills per shard-hour continuously, so a stream sized for peak carries that cost overnight and on weekends alike. A production Kafka cluster typically runs three brokers with replication factor 3 and min.insync.replicas set to 2, which means three copies of every byte and three sets of instance hours before a single consumer is deployed.

The second line item surprises people: cross-availability-zone network transfer. Replication across zones is what makes the cluster survive a zone failure, and on the major clouds inter-zone traffic is billed. Rack-aware replica placement plus fetch-from-closest-replica reduces the read side of that bill. Compression on the producer, zstd or lz4, reduces both network and storage at a modest CPU cost and is close to free money on text-heavy payloads.

Retention is the third lever, and the one that moves both cost and capability the most. Kafka's default retention is 168 hours. Kinesis Data Streams defaults to 24 hours and extends to 365 days at additional cost. Retention is what makes replay possible, so cutting it to save storage quietly removes the ability to reprocess a bug. Tiered storage, which moves older log segments to object storage, is the way to keep long retention without keeping it on broker disk.

Federal retention constraints

Log retention is set by policy, not by the broker default

OMB Memorandum M-21-31, issued August 27, 2021, directs federal agencies to retain event logs for 12 months in active storage plus 18 months in cold storage, a 30-month total. DFARS 252.204-7012 requires contractors to preserve and protect relevant monitoring and packet-capture data for at least 90 days from submission of a cyber incident report. NIST SP 800-53 Rev. 5 adds AU-4 on audit log storage capacity, AU-9 on protection of audit information, and AU-11 on record retention. On a federal system, those numbers set the retention floor and the tiered-storage design follows from them.

Change data capture is the case that usually justifies itself

Of all the reasons teams adopt streaming, keeping a secondary store in agreement with a primary database is the one that most often holds up. Reading the write-ahead log directly, through a MySQL binlog reader, PostgreSQL logical replication, or Oracle LogMiner, avoids the two classic sins of polling: the load a repeated full-table scan puts on a transactional system, and the deletes that a high-water-mark query can never see.

The design still has to be earned. Snapshot-then-stream transitions are where implementations break, since the initial consistent snapshot and the ongoing log tail have to hand off without gaps or duplicates. Schema changes on the source propagate into the stream and need a compatibility policy waiting for them. Our engineers treat the initial snapshot as a first-class part of the build rather than an afterthought, because a change-data-capture pipeline that cannot re-snapshot cleanly cannot recover from its own worst day.

When a simpler method is the right answer

An honest list, because the strongest recommendation we give some customers is to not build the thing they asked us to price.

The output has a human cadence. A daily leadership report, a weekly reconciliation, a monthly regulatory extract. A scheduled job with an incremental high-water mark does this correctly, cheaply, and can be operated by a team without streaming on-call.

The source is a vendor API with a rate limit. No streaming architecture makes a partner endpoint answer faster than the contract allows. The interval is set upstream and the broker adds only cost.

Volume is modest and the window is long. A few million rows a day into a warehouse is a scheduled load, not a stream. Modern warehouses ingest that in minutes, and incremental models on a schedule cover the requirement with tooling the team already runs.

Correctness matters more than freshness. Financial close, actuarial runs, and anything a reviewer will trace line by line benefit from a batch's clean boundaries. A deterministic re-run over a fixed window is easier to defend to an auditor than a replayed stream.

There is also an honorable middle: incremental micro-batch every one to five minutes. It covers a surprising share of what gets specified as real-time, costs a fraction of continuous streaming to operate, and stays legible to whoever inherits it. When a requirement reads "near real-time" without a number attached, micro-batch is usually what the requester meant.

A build sequence that keeps options open

  • Write the interval requirement as a number with a named consumer before selecting any tool.
  • Fix the partition key and the event schema first, and review both with the teams who will produce into them.
  • Make every sink write idempotent by natural key on day one, not after the first duplicate incident.
  • Set state TTL, allowed lateness, and a late-record side output in the first version of every windowed job.
  • Ship the canary heartbeat and the lag-in-seconds alert before the first production producer connects.
  • Load-test at two times projected peak and record the measured drain time after a simulated outage.
  • Set retention from the governing policy floor, then design tiered storage to hold it affordably.
  • Document the replay procedure and rehearse it once before go-live, with the runbook the on-call engineer will actually open.

Common objections

Our leadership asked for real-time. Can we push back?

Yes, with numbers rather than opinion. Present the three-point cost curve: what a decision costs at five minutes stale, one hour stale, and one day stale. Alongside it, present the annual operating cost of each shape. In our experience the conversation resolves quickly once the requester sees that a five-minute interval delivers the same decision quality as a five-second one for their use case, at a small fraction of the run rate.

Does managed infrastructure remove the operational burden?

It removes broker patching and cluster provisioning, which is real value. It does not remove partition-key design, schema governance, watermark tuning, state sizing, replay procedure, or lag alerting. Those are application concerns, and they are where most streaming incidents originate. Managed services move the work up the stack rather than eliminating it.

Can we start with batch and add streaming later?

Often yes, and it is usually the right sequence. The move is much easier if the batch design already carries an immutable event record with a stable natural key and an event timestamp, because that is the same contract a stream needs. Teams that build batch on mutable snapshots with no event identity pay for the migration twice.

How do streaming systems fit a FedRAMP or IL boundary?

The broker, the state backend, and the object storage behind tiered retention are all in the authorization boundary and appear in the System Security Plan. The parts that get missed are the connectors reaching outside the boundary and the dead-letter queue, which frequently holds the same sensitivity of data as the main topic while receiving a fraction of the control attention. Both belong in the boundary diagram from the start.

Bottom line

Streaming is a legitimate and sometimes mandatory shape, and the teams that succeed with it treat it as a distributed-systems commitment rather than a faster pipeline. The commitment is real: partition design, delivery semantics, state discipline, replay rehearsal, and an on-call rotation that understands lag. Where the requirement earns that commitment, the result is worth it. Where it does not, an incremental job on a schedule delivers the same decision to the same person at a small fraction of the cost, and stays running after the people who built it move on.

Frequently asked questions

How do you tell whether a workload needs streaming?

Write down the consumer of the output, the decision they make, and the cost of that decision on data that is five minutes, one hour, and one day old. When the cost is flat between five minutes and an hour, a scheduled or micro-batch job meets the requirement. The curve bends where an action is irreversible, where the data has a short useful life, or where a secondary index must agree with a source system continuously.

Is exactly-once processing real?

Within a bounded set of participating systems, yes. Kafka has offered idempotent producers and transactions since 0.11, and Flink commits checkpoints to sinks through a two-phase protocol. The guarantee ends at any sink that does not participate in the transaction. The durable engineering answer is at-least-once delivery with idempotent writes keyed on a natural identifier.

What metrics indicate a healthy streaming pipeline?

Consumer lag expressed in seconds rather than records, end-to-end latency measured by an injected canary event, watermark skew against wall-clock time, checkpoint duration relative to checkpoint interval, dead-letter queue depth and oldest-message age, and sustained utilization at or below about 60 percent of measured capacity.

How long should we retain events?

On a federal system the policy floor governs. OMB M-21-31 directs 12 months of active plus 18 months of cold storage for event logs, and DFARS 252.204-7012 requires 90 days of preserved monitoring data after a cyber incident report. Beyond compliance, retention determines whether a bug can be fixed by replay, so it should exceed the realistic time to detect and correct a logic error.

What makes streaming expensive compared with batch?

Capacity that bills continuously whether or not data flows, replication that stores three copies of every byte, and cross-availability-zone network transfer for that replication. Batch compute bills only while a job runs. Compression, rack-aware replica placement, and tiered storage for older segments are the three levers that move the bill most.

1 business day response

Deciding whether your next system should stream?

We design, build, and operate ingestion for federal, state, and commercial customers, batch through continuous streaming, and we will tell you plainly when a scheduled job is the better engineering.

CapabilitiesMore insights →Start a conversation
UEI Y2JVCZXT9HP5CAGE 1AYQ0NAICS 541512SAM.GOV ACTIVE