sh4.tech

Tip 3 min read Kafka · configuration

An old broker list that returned 504s

A screen in a finance system answered 504. The cause was a Kafka broker list that still pointed to a cluster replaced months before. Why the client didn't complain at startup, and how to make it fail in one second.

When it happened
Published
Measured
, production log and the producer config in the repository; the 12 s is computed from the config
Stack
PHP (CodeIgniter 4), php-rdkafka over librdkafka, Kafka in KRaft mode
Also in
Português

The symptom

On September 22 a screen of the finance system started to answer 504. The gateway in front of the app gives up after some seconds, so the user got a generic error page, and the application log had one line for each try:

ERROR: Local: Timed out

"Local" is the important word. In librdkafka, errors that start with Local: are produced by the client itself, before any broker says anything. The client was waiting for something and nobody answered.

The cause

The service's production config had the broker list hardcoded, like this (hostnames here, the real file had IPs):

app/Config/Boot/production.php, before
defined('KAFKA_BROKER_PRODUCER') || define('KAFKA_BROKER_PRODUCER', ["kafka-old-1:9092", "kafka-old-2:9092"]);

That list came from the first commit of the project. In May the company moved to a new cluster with three brokers, and the services were repointed one by one; this one kept the old list, and the old brokers had become unreachable. The fix was one line, the same list that the other services already used in production:

after
defined('KAFKA_BROKER_PRODUCER') || define('KAFKA_BROKER_PRODUCER', ["kafka-1:9092", "kafka-2:9092", "kafka-3:9092"]);

Why nothing complained at startup

The library had a check that looked right:

the check that never fires
$ok = $producer->addBrokers(implode(",", $brokers));
if ($ok == 0)
    throw new KafkaException("Could not connect to any broker");

addBrokers() returns how many brokers were added to the client's list. It parses the string and returns. The connection happens later, in the background, so with two dead brokers it returns 2 and the exception never fires.

Then each send did a poll(3000) and a flush(3000) with up to three retries. With no broker alive that is around 12 seconds before the error, for one message. Add a second send, or the flush that runs again in the destructor, and the request passes the gateway timeout. The user sees a 504 and the real error stays in a log nobody was reading.

Make it fail in one second

If you want to know at startup that a broker answers, ask for metadata. getMetadata() needs a real broker and throws when the timeout passes:

real connectivity check
try {
    $producer->getMetadata(false, null, 1000);   // 1 s, no topic list
} catch (\RdKafka\Exception $e) {
    throw new KafkaException("No Kafka broker answered: " . $e->getMessage());
}

Running this on every request is too much, a cached health check is enough. And for anything a user waits for, keep the flush short and return a clear error, or write the message to an outbox table and send it outside the request, so a slow Kafka becomes a delayed message and the screen still works.

What I took from it

  • A broker list per service, copied by hand, drifts. One shared place (an environment variable from the same source, or a DNS name per broker) moves with the cluster.
  • addBrokers() counts strings. A metadata request is what proves a connection.
  • Local: Timed out means the client never heard back. Look at the network and the config before the broker.
  • A synchronous produce inside an HTTP request turns any Kafka problem into the gateway's problem, with a worse message.
If you take one thingWhen a cluster moves, search every repository for the old addresses, including the services nobody remembers. Here the list stayed in a file that nobody had a reason to open since the first commit.

Filed under the problem it solves

Luciano Serra
Luciano Serra Founder-engineer · co-founder & CTO, built a 15,000-screen network from zero, 2015–2024 · São Paulo, remote

I design production systems from zero and I take over the ones already running. Either way they have to hold up when the business grows. I co-founded a digital out-of-home company and built its technology for ten years; my partners raised the money and ran sales.

Your business is growing faster than your systems? Let's talk →