Skip to content

AxonOps — AI-Native Control Plane for Open Source Data Platforms

Shock Absorber Pattern

The shock absorber pattern uses Kafka as a buffer between systems with different throughput capacities. Kafka absorbs traffic spikes, allowing downstream systems to process at their own sustainable rate without being overwhelmed.


Backend systems—databases, mainframes, third-party APIs—do not need to be provisioned for peak capacity. They only need to handle the average throughput, with Kafka absorbing the difference during spikes.

ComponentWithout KafkaWith Kafka
DatabaseProvisioned for peak loadProvisioned for average load
Application serversAuto-scale to handle spikesFixed pool at steady capacity
Downstream APIsRate limiting / request rejectionSteady, predictable load

This translates directly to:

  • Lower infrastructure costs — Smaller database instances, fewer application servers
  • Predictable performance — No degradation during traffic spikes
  • Simplified capacity planning — Plan for average, not peak
  • Reduced operational complexity — No auto-scaling policies to tune

The shock absorber pattern introduces latency between when data is produced and when it reaches the backend system. During traffic spikes, this delay can grow significantly as the buffer fills.

The pattern works best for short-lived spikes where the buffer can drain during quieter periods:

Spike PatternSuitabilityReason
Flash sales (minutes to hours)ExcellentBuffer drains overnight
Daily peaks (predictable hours)ExcellentOff-peak hours allow catch-up
Sustained high load (days/weeks)PoorBuffer never drains, lag grows indefinitely
Gradual permanent increasePoorRequires capacity increase, not buffering

Before adopting this pattern, assess whether traffic spikes are temporary or represent a sustained increase in load. If the latter, the solution is to scale the backend, not buffer indefinitely.

Systems where delayed updates are acceptable:

  • Analytics and reporting pipelines
  • Search index updates
  • Data warehouse loading
  • Notification delivery
  • Audit log processing
  • Cache warming
  • Real-time transaction processing requiring immediate confirmation
  • Systems where users expect instant visibility of changes
  • Low-latency trading or bidding systems
  • Sustained load increases (requires actual scaling)

A car’s suspension system provides the perfect analogy. When the wheel hits a speedbump (traffic spike), the spring compresses (Kafka buffers messages) and the damper controls the release rate (consumer processes at steady pace). The car body (backend system) experiences a smooth ride regardless of road conditions.

Spring and Damper AnalogySpring and Damper AnalogyRoad Surface(Input Traffic)Suspension System(Kafka)Car Body(Backend System)Speedbump = Traffic SpikesSpring(Buffer capacity)Damper(Controlled release)Smooth ride(Steady load)Spring stores energy (messages)Damper releases gradually (consumer rate)Result: Passengers feel smooth motionBumpy inputSmoothed output

In signal processing terms, Kafka acts as a low-pass filter (or IIR filter). High-frequency spikes in the input signal are attenuated, while the underlying trend passes through smoothly to the output.

Input signal: producer message rate with spikesInput signal: producer message rate with spikes020004000600080001000012000Rate (msg/s)t0t1t2t3t4t5t6t7t8Time
Output signal: consumer message rate smoothed by KafkaOutput signal: consumer message rate smoothed by Kafka020004000600080001000012000Rate (msg/s)t0t1t2t3t4t5t6t7t8Time

Kafka filters out high-frequency variations. The consumer sees a steady, predictable load while spikes are absorbed into the buffer (consumer lag).

The following chart illustrates the smoothing effect in practice:

Throughput Smoothing: Kafka as Shock AbsorberThroughput Smoothing: Kafka as Shock Absorber02000400060008000100001200014000Messages/sec00:0003:0006:0009:0012:0015:0018:0021:0024:00TimeProducer (Input)Consumer (Output)
TimeProducer RateConsumer RateLag TrendBackend Load
00:001,000/s1,000/s1,000/s
06:002,000/s2,000/s2,000/s
12:0012,000/s2,000/s↑↑ Peak2,000/s
18:003,000/s2,000/s↓ Draining2,000/s
24:001,000/s1,000/s— Empty1,000/s

Key insight: The area between producer and consumer rates represents buffered messages (consumer lag). This lag grows during spikes and drains during quiet periods. The backend database experiences a constant, manageable load while Kafka absorbs all variability.


Systems rarely produce and consume data at identical rates:

The Rate Mismatch ProblemThe Rate Mismatch ProblemWithout BufferWith Kafka BufferProducer(10,000 msg/s burst)Consumer(1,000 msg/s max)Producer(10,000 msg/s burst)KafkaConsumer(1,000 msg/s max)Direct connectionConsumer overwhelmedRequests rejectedData lostBurst absorbedSteady 1,000 msg/sConsumer healthyNo data loss

Common scenarios:

ScenarioProducer RateConsumer CapacityProblem
Flash sales100x normalFixed database IOPSDatabase saturation
Log ingestionVaries with trafficFixed Elasticsearch clusterIndex rejections
IoT telemetrySensor burstsLimited analytics pipelineProcessing delays
Batch jobsBulk exportsRate-limited APIsAPI throttling
Event-drivenCascading eventsLegacy system limitsSystem crashes

Kafka’s architecture naturally supports load leveling:

Kafka as Shock AbsorberKafka as Shock AbsorberProducersKafkaConsumersProducers(variable rate)Producers(variable rate)Kafka(buffer)Kafka(buffer)Consumers(fixed rate)Consumers(fixed rate)Traffic Spike10,000 msg/sMessages bufferedLag increases1,000 msg/s (max capacity)Normal Traffic500 msg/sLag decreasesBuffer drains500 msg/sSpike Again8,000 msg/sBuffer absorbsConsumer unaffected1,000 msg/sKafka retention = buffer sizeConsumer lag = buffer utilization

Key properties:

  1. Durable buffer - Messages persist until consumed (or retention expires)
  2. Decoupled rates - Producers and consumers operate independently
  3. Horizontal scaling - Add partitions for higher throughput
  4. Configurable retention - Buffer size measured in time or bytes

Legacy systems (mainframes, older databases) often cannot scale elastically. Kafka shields them from modern traffic patterns.

Protecting Legacy BackendProtecting Legacy BackendModern FrontendLegacy BackendWeb AppMobile AppAPI GatewayMainframe(500 TPS max)Oracle DB(1000 TPS max)Kafka(shock absorber)Rate-LimitedConsumerConsumer enforces rate limits:- Token bucket algorithm- Pause/resume based on lag- Circuit breaker for backendControlled rate(400 TPS)Controlled rate(800 TPS)

Implementation:

public class RateLimitedConsumer {
private final RateLimiter rateLimiter;
private final LegacyClient legacyClient;
private final Consumer<String, Event> consumer;
public RateLimitedConsumer(int maxTps) {
this.rateLimiter = RateLimiter.create(maxTps);
}
public void consume() {
while (running) {
ConsumerRecords<String, Event> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, Event> record : records) {
// Block until rate limit allows
rateLimiter.acquire();
try {
legacyClient.process(record.value());
} catch (RateLimitException e) {
// Backend is struggling - pause consumption
consumer.pause(consumer.assignment());
Thread.sleep(backoffMs);
consumer.resume(consumer.assignment());
}
}
consumer.commitSync();
}
}
}

Collect events during high-traffic periods, process in efficient batches during low-traffic periods.

Batch Aggregation PatternBatch Aggregation PatternThroughout the DayBatch ProcessorEvents(continuous)Nightly Job(2 AM - 4 AM)Kafka(24h retention)Data WarehouseRetention configured for batch window.Messages accumulate until batch runs.Stream in(millions/day)Batch consume(process all at once)Bulk load(efficient)

Configuration:

# Topic retention sized for batch window
log.retention.hours=48
log.retention.bytes=-1
# Consumer for batch processing
max.poll.records=10000
fetch.max.bytes=52428800
enable.auto.commit=false

Different consumers process the same events at different rates based on their capabilities.

Multi-Speed ConsumersMulti-Speed ConsumersFast ConsumerMedium ConsumerSlow ConsumerCache Update(10,000 msg/s)Search Index(2,000 msg/s)Analytics DB(500 msg/s)Order Serviceorders.events(12 partitions)Each consumer group:- Independent offset tracking- Processes at own rate- Lag reflects buffer usageconsumer-group:cache-updaterconsumer-group:search-indexerconsumer-group:analytics-loader

In the shock absorber pattern, consumer lag is not a problem—it’s the buffer working as intended.

Consumer Lag During Traffic SpikeConsumer Lag During Traffic SpikeTraffic PatternTimeProducer RateConsumer RateLag00:00500/s500/s012:00500/s500/s014:00 5,000/s 1,000/s +4,000/s 14:30 5,000/s 1,000/s +4,000/s 15:00500/s1,000/s -500/s 17:00500/s1,000/s0 (drained)Lag accumulated: 14:00-15:00 = 4,000 * 3,600 = 14.4M messagesDrain time: 14.4M / 500 = 8 hoursBuffer must retain messages for drain period

Calculate required retention based on expected spike patterns:

Buffer Size = (Peak Rate - Consumer Rate) × Spike Duration
Example:
- Peak rate: 10,000 msg/s
- Consumer rate: 2,000 msg/s
- Spike duration: 2 hours
Buffer = (10,000 - 2,000) × 7,200 seconds = 57.6 million messages
With 1KB average message size:
Storage needed = 57.6M × 1KB = ~58 GB per partition

Retention configuration:

# Time-based retention (for predictable spike patterns)
log.retention.hours=72
# Size-based retention (for storage constraints)
log.retention.bytes=107374182400 # 100 GB per partition
# Combined (whichever triggers first)
log.retention.hours=72
log.retention.bytes=107374182400
# Prometheus alerting rules for shock absorber pattern
groups:
- name: kafka-shock-absorber
rules:
# Alert on lag growth rate, not absolute lag
- alert: ConsumerLagGrowingTooFast
expr: |
rate(kafka_consumer_group_lag[5m]) > 1000
for: 15m
labels:
severity: warning
annotations:
summary: "Consumer lag growing rapidly"
description: "Lag increasing by {{ $value }}/s - verify this is expected"
# Alert if lag approaches retention limit
- alert: ConsumerLagApproachingRetention
expr: |
kafka_consumer_group_lag / kafka_topic_partition_current_offset
> 0.8
for: 30m
labels:
severity: critical
annotations:
summary: "Consumer may lose messages"
description: "Lag at 80% of available buffer - messages at risk"
# Alert if lag not draining during off-peak
- alert: ConsumerLagNotDraining
expr: |
kafka_consumer_group_lag > 1000000
and hour() >= 2 and hour() <= 6
for: 2h
labels:
severity: warning
annotations:
summary: "Lag not draining during off-peak"
description: "Expected lag to decrease overnight"

When consumers cannot keep up even during normal periods, implement backpressure.

public class BackpressureConsumer {
private static final long LAG_THRESHOLD = 1_000_000;
private static final long RESUME_THRESHOLD = 100_000;
private final AdminClient adminClient;
private boolean paused = false;
public void consumeWithBackpressure() {
while (running) {
long currentLag = getCurrentLag();
if (!paused && currentLag > LAG_THRESHOLD) {
// Too far behind - pause to let downstream recover
consumer.pause(consumer.assignment());
paused = true;
notifyUpstream("SLOW_DOWN");
log.warn("Paused consumption - lag {} exceeds threshold", currentLag);
}
if (paused && currentLag < RESUME_THRESHOLD) {
// Caught up enough to resume
consumer.resume(consumer.assignment());
paused = false;
notifyUpstream("READY");
log.info("Resumed consumption - lag {} below threshold", currentLag);
}
if (!paused) {
ConsumerRecords<String, Event> records = consumer.poll(Duration.ofMillis(100));
processRecords(records);
} else {
// Still paused - sleep to avoid busy loop
Thread.sleep(1000);
}
}
}
}
public class AdaptiveBatchConsumer {
private int currentBatchSize = 1000;
private static final int MIN_BATCH = 100;
private static final int MAX_BATCH = 10000;
public void consumeAdaptively() {
while (running) {
ConsumerRecords<String, Event> records = consumer.poll(Duration.ofMillis(100));
long startTime = System.currentTimeMillis();
int processed = processBatch(records, currentBatchSize);
long duration = System.currentTimeMillis() - startTime;
// Adjust batch size based on processing time
if (duration < 100 && currentBatchSize < MAX_BATCH) {
// Processing fast - increase batch size
currentBatchSize = Math.min(currentBatchSize * 2, MAX_BATCH);
} else if (duration > 1000 && currentBatchSize > MIN_BATCH) {
// Processing slow - decrease batch size
currentBatchSize = Math.max(currentBatchSize / 2, MIN_BATCH);
}
consumer.commitSync();
}
}
}

Route high-priority messages to a fast lane that’s always processed, while low-priority messages buffer.

Priority Lanes PatternPriority Lanes PatternConsumerHigh PriorityProcessorNormalProcessorBulkProcessorProducerorders.high-priorityorders.normalorders.bulkPriority RouterProcessing priority:1. High - never buffered2. Normal - short buffer OK3. Bulk - long buffer expectedVIP ordersRegular ordersBatch importsAlways processedimmediatelyProcessed whenhigh queue emptyProcessed duringoff-peak only

The shock absorber pattern enables significant cost savings by smoothing resource usage.

Auto-Scaling Cost PatternAuto-Scaling Cost PatternTrafficAuto-Scaled InstancesPeak: 10,000 req/s (2 hours/day)Normal: 1,000 req/s (22 hours/day)Peak: 50 instances × 2 hours = 100 instance-hoursNormal: 5 instances × 22 hours = 110 instance-hoursTotal: 210 instance-hours/day Must scale UP before spike hitsScale-up latency = potential failures
Shock Absorber Cost PatternShock Absorber Cost PatternTrafficFixed Consumer PoolPeak: 10,000 msg/s → KafkaNormal: 1,000 msg/s → KafkaSteady: 10 instances × 24 hours = 240 instance-hours But: smaller instance type (handles 1,500/s each)No auto-scaling complexityNo scale-up latency riskPredictable, lower costKafka Buffer

Cost comparison:

ApproachCompute CostOperational ComplexityRisk
Auto-scalingHigher (peak capacity)High (scaling policies)Scale-up latency
Shock absorberLower (steady capacity)Low (fixed pool)Must size buffer

Anti-Pattern: Undersized BufferAnti-Pattern: Undersized BufferProblemSolutionRetention: 1 hourSpike duration: 4 hoursConsumer rate: 1,000/s Result: Messages expire before consumedData loss during extended spikesCalculate worst-case spikeAdd 50% safety marginSet retention accordinglyMonitor buffer utilization
// WRONG: Assuming consumer keeps up
@KafkaListener(topics = "events")
public void consume(Event event) {
// No rate limiting
// No health checks
// No backpressure
database.insert(event); // What if DB is slow?
}
// RIGHT: Health-aware consumption
@KafkaListener(topics = "events")
public void consume(Event event, Acknowledgment ack) {
if (!healthChecker.isDownstreamHealthy()) {
// Don't ack - message will be redelivered
throw new RetryableException("Downstream unhealthy");
}
rateLimiter.acquire();
try {
database.insert(event);
ack.acknowledge();
} catch (Exception e) {
// Let Kafka retry
throw new RetryableException(e);
}
}
Anti-Pattern: No Drain StrategyAnti-Pattern: No Drain StrategyProblemSolutionLag accumulates during peakConsumer runs at same rate 24/7Lag never fully drainsBuffer keeps growing until retention hitScale consumers during off-peakOr: higher consumer throughput alwaysOr: accept occasional data loss (if OK) Buffer must drain before next spike

AspectRecommendation
Buffer sizingPeak rate × spike duration × safety margin
RetentionTime or size based on drain requirements
Consumer rateMust exceed average producer rate
Lag alertingAlert on growth rate, not absolute value
BackpressurePause/resume, adaptive batching, or priority lanes
MonitoringBuffer utilization, drain time, rate delta

The shock absorber pattern transforms unpredictable traffic into predictable processing load, enabling cost optimization, protecting legacy systems, and improving overall system resilience.