{"id":"c0b4703c7af2b9ec","repo":"apache/kafka","slug":"requested-metadata-update-after-close","errorCode":null,"errorMessage":"Requested metadata update after close","messagePattern":"Requested metadata update after close","errorType":"exception","errorClass":"KafkaException","httpStatus":null,"severity":"critical","filePath":"clients/src/main/java/org/apache/kafka/clients/producer/internals/ProducerMetadata.java","lineNumber":169,"sourceCode":"    /**\n     * Wait for metadata update until the current version is larger than the last version we know of\n     */\n    public synchronized void awaitUpdate(final int lastVersion, final Timer timer) throws InterruptedException {\n        while (true) {\n            // Throw fatal exceptions, if there are any. Recoverable topic errors will be handled by the caller.\n            maybeThrowFatalException();\n            if (updateVersion() > lastVersion || isClosed())\n                break;\n\n            timer.update();\n            if (timer.isExpired())\n                throw new TimeoutException(\"Failed to update metadata after \" + timer.timeoutMs() + \" ms.\");\n\n            wait(timer.remainingMs());\n        }\n\n        if (isClosed())\n            throw new KafkaException(\"Requested metadata update after close\");\n    }\n\n    @Override\n    public synchronized void update(int requestVersion, MetadataResponse response, boolean isPartialUpdate, long nowMs) {\n        super.update(requestVersion, response, isPartialUpdate, nowMs);\n        errors = response.errors();\n\n        // Remove all topics in the response that are in the new topic set. Note that if an error was encountered for a\n        // new topic's metadata, then any work to resolve the error will include the topic in a full metadata update.\n        if (!newTopics.isEmpty()) {\n            for (MetadataResponse.TopicMetadata metadata : response.topicMetadata()) {\n                newTopics.remove(metadata.topic());\n            }\n        }\n\n        notifyAll();\n    }\n","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/apache/kafka/blob/c31c9215e131f8c17e79f8901b48c13ee6aa8e7a/clients/src/main/java/org/apache/kafka/clients/producer/internals/ProducerMetadata.java#L151-L187","documentation":"Thrown by ProducerMetadata.awaitUpdate after the loop breaks because isClosed() became true while waiting for a metadata version bump. It indicates a race where the application closed the producer (or it was closed by an error path) while a send() call was still blocked on metadata. The producer refuses to serve metadata to a closed instance because further sends would be impossible anyway.","triggerScenarios":"One thread calls producer.close() while another thread is inside producer.send() blocked in awaitUpdate(); or close() races with an inflight metadata refresh triggered by an unknown topic. Also reachable if the producer's metadata instance is closed by a fatal error path concurrently with send().","commonSituations":"Concurrent use of a KafkaProducer from multiple threads where one thread tears it down (shutdown hook, container stop, bean @PreDestroy); producers shared across request threads in a web app that also closes on undeploy; mis-ordered shutdown that closes the producer before draining in-flight sends.","solutions":["Ensure no thread is calling send() when close() is invoked: drain in-flight sends (flush + callback completion) before close.","Do not share a single KafkaProducer instance across threads that can independently close it; own the lifecycle in one place.","Use a coordinated shutdown (e.g. close(Duration)) after a final flush() and after all send-calling workers have stopped.","Catch and treat this exception as a shutdown signal rather than a retryable error — the producer is gone."],"exampleFix":"// before\nnew Thread(() -> producer.send(rec)).start();\nproducer.close(); // races with the send above\n\n// after\nproducer.send(rec, cb -> { latch.countDown(); });\nproducer.flush();\nlatch.await();\nproducer.close(Duration.ofSeconds(10));","handlingStrategy":"validation","validationCode":"// KafkaProducer does not expose isClosed(); track it yourself at the single ownership point.\nprivate final java.util.concurrent.atomic.AtomicBoolean closed = new java.util.concurrent.atomic.AtomicBoolean();\n...\nif (closed.get()) throw new IllegalStateException(\"producer already closed\");\nproducer.partitionsFor(topic); // or any metadata-dependent call","typeGuard":null,"tryCatchPattern":"try {\n    producer.partitionsFor(topic);\n} catch (org.apache.kafka.common.KafkaException ke) {\n    if (ke.getMessage() != null && ke.getMessage().contains(\"after close\")) {\n        // producer was closed concurrently; stop using it\n    }\n}","preventionTips":["Centralize producer lifecycle: one owner constructs, uses, and closes it; no other code path can race close().","Set a closed flag before invoking close() and check it before every metadata-dependent call.","Await outstanding futures / callbacks before close() so no metadata refresh can fire afterwards."],"tags":["producer","lifecycle","concurrency","metadata","java"],"analyzedSha":"c31c9215e131f8c17e79f8901b48c13ee6aa8e7a","analyzedAt":"2026-08-03T12:34:05.770Z","schemaVersion":2}