Introducing uFowarder: The Consumer Proxy for Kafka Async Queuing

Zhifeng Chen, Yang Yang, Haifeng Chen
12 min readintermediate
--
View Original

Overview

This article introduces uForwarder, Uber's open-source push-based consumer proxy for Apache Kafka's async queuing system. It details the production challenges encountered after scaling to over 1,000 consumer services, including head-of-line blocking, hardware efficiency, delay processing, and message isolation, along with the solutions implemented such as context-aware routing, active head-of-line blocking resolution, consumer auto rebalancing, and a DelayProcessManager.

What You'll Learn

1

How to build a push-based consumer proxy that abstracts Kafka consumer complexity behind a gRPC interface

2

How to detect and mitigate head-of-line blocking in Kafka message queuing using out-of-order commit trackers and dead-letter queues

3

How to implement context-aware routing for production/non-production and zone isolation in Kafka messaging

4

How to design adaptive workload sizing and placement for efficient hardware utilization in a consumer proxy fleet

5

How to implement partition-level delay processing without blocking the entire fetcher thread

Prerequisites & Requirements

  • Understanding of Apache Kafka concepts including topics, partitions, consumer groups, and offset management
  • Familiarity with gRPC protocol and Protobuf for service-to-service communication
  • Understanding of distributed systems concepts including load balancing, message queuing, and availability zones
  • Experience operating Kafka consumers at scale in a production environment(optional)

Key Questions Answered

What is uForwarder and how does it work as a Kafka consumer proxy?
uForwarder is Uber's open-source push-based consumer proxy for Kafka async queuing. It fetches messages from Kafka using the binary protocol, pushes each message individually to consumer service instances via gRPC endpoints, receives processing results as gRPC status codes, aggregates the results, and commits proper offsets back to Kafka when safe. It abstracts away Kafka consumer management complexity from service owners.
How does uForwarder detect and resolve head-of-line blocking in Kafka consumers?
uForwarder uses an out-of-order commit tracker to detect head-of-line blocking by monitoring two conditions: tracker utilization exceeding 90% and uncommitted message percentage falling below 2%. When detected, it mitigates by marking the blocking offset as CANCELED, canceling associated gRPC requests including retries, sending the message to a dead-letter queue, and marking the offset as COMMITTED to unblock the queue.
What causes poison-pill messages that fail in transit before reaching the consumer handler?
Messages can fail in transit due to three main causes: message size limits where gRPC servers default to a 4MB max payload causing oversized messages to never reach the handler, invalid consumer instances where targeted instances are non-functional under traffic isolation, and request filtering where interceptors built into consumer services fail requests before they reach the consumer handler.
How does context-aware routing enable message isolation in Kafka consumer proxy?
The producer service injects subsetting context (such as zone or production/non-production metadata) into Kafka message headers. Consumer Proxy converts this context from the message header to a gRPC request header. The load balancer then performs context-aware routing based on the request header, directing messages to appropriate consumer instances without requiring separate Kafka topics for each isolation dimension.
How does uForwarder handle workload sizing and placement for hardware efficiency?
Consumer Proxy uses adaptive workload sizing by observing traffic metrics across CPU, memory, and network dimensions to determine resource needs for each workload. It continuously runs convergence procedures with asymmetric time windows—fast scale-up and slow scale-down—to minimize consumer lag from insufficient resources while reducing message duplication from placement shuffles. Workloads of different sizes are bin-packed onto fixed-size worker instances.
How does DelayProcessManager implement delay processing without blocking the fetcher thread?
DelayProcessManager pauses only individual topic partitions that haven't met their delay requirement using Kafka's native pause/resume API, rather than blocking the entire fetcher thread. It stores polled but unprocessed messages in an in-memory buffer to prevent redundant polling. Before each poll cycle, it checks and resumes partitions that have met their delay, merging buffered messages with newly polled ones. This guarantees at-least delay, not exact delay.
Why is zone isolation important for Kafka message consumers at Uber?
Uber services run across multiple regions with multiple availability zones each. Without zone isolation, a producer service failure in one zone could cause consumer service failures in any zone. Zone isolation confines messages to the same zone across producer and consumer services, minimizing the blast radius of zone failures and improving overall system resilience.
What are the next planned features for uForwarder?
Two major features are planned: consumer lag mitigation through rewinding Kafka consumer offsets to latest while spinning up side consumers to catch up on lagging data (balancing data freshness and completeness), and native Protobuf data format support so consumer services receive Protobuf objects directly instead of raw Kafka bytes requiring manual decoding.

Key Statistics & Figures

Kafka deployment scale
Trillions of messages and multiple petabytes of data per day
Uber's Apache Kafka deployment, one of the largest in the world
Consumer services onboarded
Over 1,000
Consumer services using the Consumer Proxy as the primary Kafka pub-sub option at Uber
Default gRPC max payload size
4MB
Default gRPC server max request payload size, messages exceeding this become poison-pill messages
Head-of-line blocking utilization threshold
90%
Tracker utilization threshold used in production for detecting head-of-line blocking
Head-of-line blocking uncommitted percentage threshold
2%
Uncommitted message percentage threshold used in production for detecting head-of-line blocking
Fleet scale progression
Two digits to hundreds to thousands of servers
Growth of Consumer Proxy fleet from early days through scaling as more consumers migrated

Technologies & Tools

Some links below are affiliate links. We may earn a commission if you make a purchase.

Key Actionable Insights

1
Use out-of-order commit tracking with dual-threshold detection (utilization > 90%, uncommitted < 2%) to actively identify head-of-line blocking situations. This approach catches blocking before it stalls the entire consumer, and the combination of both metrics avoids false positives during normal high-load operation.
Head-of-line blocking is defined as a small number of messages blocking the majority, so a high tracker utilization combined with very few uncommitted messages is a strong signal. The mitigation procedure should cancel in-flight requests and redirect to a dead-letter queue.
2
Implement context-aware routing through message headers rather than splitting Kafka topics when you need message isolation. By injecting subsetting context (zone, environment) into Kafka message headers and converting them to gRPC request headers, you achieve isolation without breaking backward compatibility for other consumers like streaming analytics or data ingestion pipelines.
Splitting topics was considered but rejected because a single topic is often consumed by multiple systems beyond messaging. Header-based routing preserves the single-topic model while enabling flexible routing at the proxy layer.
3
Use asymmetric time windows for workload scaling—fast scale-up and slow scale-down—to balance responsiveness with stability. Fast scale-up minimizes consumer lag from insufficient resources, while slow scale-down prevents unnecessary workload placement shuffles that cause message duplication.
This applies to any adaptive auto-scaling system where the cost of under-provisioning (consumer lag) is higher than the cost of temporary over-provisioning. The Consumer Proxy controller continuously runs convergence procedures to match computed scale with actual scale.
4
When implementing delay processing in Kafka, pause individual partitions rather than blocking the entire fetcher thread. Use Kafka's native pause/resume API to selectively hold partitions that haven't met their delay requirement while allowing other partitions to continue processing, and buffer polled but unprocessed messages in memory to avoid redundant polling.
The previous approach of pausing the entire fetcher thread limited retry topics to a single partition for scalability. The partition-level approach removes this restriction, enabling multi-partition retry topics and better throughput.
5
Design your delay processing to guarantee at-least delay rather than exact delay, and communicate this semantic clearly to consumers. The actual delay can exceed the predefined time when the current batch processing time is longer than the delay period, so consumers should not rely on precise timing.
This is inherent to batch-oriented systems where resuming paused partitions happens only after workers complete processing the current batch. Consumers needing exact timing should use dedicated scheduling systems instead.
6
Make workload placement sticky after initial assignment, only triggering rebalance when a workload size violation or worker liveness check failure is detected. This minimizes the disruption caused by unnecessary consumer rebalances, which can cause message duplication and temporary processing gaps.
Consumer Proxy bin-packs multiple workloads of varying sizes onto fixed-size worker instances, and each workload's resource needs are calculated adaptively across CPU, memory, and network dimensions based on observed traffic metrics.

Common Pitfalls

1
Relying solely on dead-letter queues to handle all poison-pill messages. Dead-letter queues only work when messages reach the consumer handler, but messages can fail in transit due to size limits (over 4MB), invalid targeted consumer instances, or request interceptor filtering—none of which reach the handler to trigger dead-letter logic.
This is why active head-of-line blocking detection and mitigation at the proxy level is necessary to complement dead-letter queues as a second layer of defense.
2
Splitting Kafka topics to achieve message isolation (production/non-production or zone-based). This approach breaks backward compatibility because topics are shared across messaging, streaming analytics, data ingestion, and other systems, requiring coordinated changes across all consumers.
Context-aware routing via message headers provides isolation without modifying the topic structure, preserving backward compatibility for all existing consumers of the topic.
3
Blocking the entire fetcher thread when implementing delay processing in Kafka. When the fetcher thread pauses completely to wait for a delay to elapse, no other partitions within that thread can be processed, forcing retry topics to be restricted to a single partition and limiting scalability.
The solution is to use Kafka's native pause/resume API at the partition level, allowing non-delayed partitions to continue processing while delayed ones wait.
4
Assuming symmetric scaling behavior is optimal for consumer proxy workloads. Using the same time window for both scale-up and scale-down leads to either consumer lag from slow scale-up or message duplication from aggressive scale-down causing unnecessary workload placement shuffles.
Use fast scale-up to respond quickly to traffic increases and slow scale-down to prevent flapping, since the cost of under-provisioning (consumer lag) is typically higher than temporary over-provisioning.
5
Routing non-production messages to production consumer services when using mixed Kafka topics. When a producer service exposes an API called by both production and non-production services, the resulting topic contains mixed messages that can fail a production service or pollute production data if not properly isolated.
Production/non-production isolation through context-aware routing ensures non-production messages are directed to non-production consumer instances, protecting production reliability and data integrity.

Related Concepts

Kafka Consumer Groups And Rebalancing
Dead-letter Queues
Head-of-line Blocking In Message Queuing
Grpc Service Mesh And Load Balancing
Out-of-order Commit Tracking
Bin Packing And Workload Placement Algorithms
Availability Zone Isolation
Consumer Lag Mitigation
Kafka Partition Pause/Resume API
Push-based Vs Pull-based Message Consumption
Poison-pill Message Detection