{"id":"bbb12db487221c82","repo":"apache/kafka","slug":"kafkaconsumer-is-not-safe-for-multi-threaded-acces-bbb12d","errorCode":null,"errorMessage":"KafkaConsumer is not safe for multi-threaded access. currentThread(name: {}, id: {}) otherThread(id: {})","messagePattern":"KafkaConsumer is not safe for multi-threaded access\\. currentThread\\(name: (.+?), id: (.+?)\\) otherThread\\(id: (.+?)\\)","errorType":"exception","errorClass":"ConcurrentModificationException","httpStatus":null,"severity":"error","filePath":"clients/src/main/java/org/apache/kafka/clients/consumer/internals/ClassicKafkaConsumer.java","lineNumber":1257,"sourceCode":"        try {\n            metadata.maybeThrowBootstrapFatalException();\n        } catch (RuntimeException e) {\n            release();\n            throw e;\n        }\n    }\n\n    /**\n     * Acquire the light lock protecting this consumer from multi-threaded access. Instead of blocking\n     * when the lock is not available, however, we just throw an exception (since multi-threaded usage is not\n     * supported).\n     * @throws ConcurrentModificationException if another thread already has the lock\n     */\n    private void acquire() {\n        final Thread thread = Thread.currentThread();\n        final long threadId = thread.getId();\n        if (threadId != currentThread.get() && !currentThread.compareAndSet(NO_CURRENT_THREAD, threadId))\n            throw new ConcurrentModificationException(\"KafkaConsumer is not safe for multi-threaded access. \" +\n                    \"currentThread(name: \" + thread.getName() + \", id: \" + threadId + \")\" +\n                    \" otherThread(id: \" + currentThread.get() + \")\"\n            );\n        refcount.incrementAndGet();\n    }\n\n    /**\n     * Release the light lock protecting the consumer from multi-threaded access.\n     */\n    private void release() {\n        if (refcount.decrementAndGet() == 0)\n            currentThread.set(NO_CURRENT_THREAD);\n    }\n\n    private void throwIfNoAssignorsConfigured() {\n        if (assignors.isEmpty())\n            throw new IllegalStateException(\"Must configure at least one partition assigner class name to \" +\n                    ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG + \" configuration property\");","sourceCodeStart":1239,"sourceCodeEnd":1275,"githubUrl":"https://github.com/apache/kafka/blob/c31c9215e131f8c17e79f8901b48c13ee6aa8e7a/clients/src/main/java/org/apache/kafka/clients/consumer/internals/ClassicKafkaConsumer.java#L1239-L1275","documentation":"Thrown as a ConcurrentModificationException from ClassicKafkaConsumer.acquire() (ClassicKafkaConsumer.java:1253) when a consumer method is entered from a thread that does not own the consumer's internal lock. KafkaConsumer is intentionally not thread-safe; instead of blocking on a second caller, the per-instance AtomicLong holding the owning thread id is compare-and-set, and on failure the library throws immediately to protect mutable state (subscriptions, fetch buffers, coordinator state, refcount). The only thread-safe method is wakeup(); every other public method (poll, subscribe, commit, seek, close, etc.) routes through acquire()/acquireAndEnsureOpen().","triggerScenarios":"A thread other than the consumer's owner thread invokes any consumer method while the owner thread is still inside one (e.g. poll still running on thread A, thread B calls commitSync or position). All public methods are guarded by acquire() at ClassicKafkaConsumer.java:1123 (close) and the acquireAndEnsureOpen() helper used by poll/subscribe/seek/commit.","commonSituations":"Sharing a single consumer across an ExecutorService or thread pool; calling consumer methods from inside a commitAsync callback (which runs on a different thread); a manual 'heartbeat' or 'offset flusher' thread; a ConsumerRebalanceListener that calls consumer methods from the rebalance thread; close() racing with poll(); framework code (Spring, Quarkus) that hands the consumer to worker threads.","solutions":["Restrict all consumer calls to the single thread that created the consumer; have other threads enqueue work items and let the owning thread execute them.","Never touch the consumer inside commitAsync's callback — only record the result (a future/atomic) and act on the owning thread.","To interrupt a blocked poll() from another thread, use consumer.wakeup() (the one thread-safe method); the owner thread then sees WakeupException and can shut down cleanly.","If concurrent processing is genuinely required, run one KafkaConsumer instance per thread and partition work externally rather than sharing one instance."],"exampleFix":"// before\nExecutorService es = Executors.newFixedThreadPool(4);\nKafkaConsumer<String,String> c = new KafkaConsumer<>(props);\nes.submit(() -> c.poll(Duration.ofSeconds(1)));\nes.submit(() -> c.commitSync());          // throws ConcurrentModificationException\n\n// after — only the owning thread touches the consumer\nwhile (running) {\n    ConsumerRecords<String,String> recs = c.poll(Duration.ofSeconds(1));\n    recs.forEach(r -> es.submit(() -> handle(r)));   // processing off-thread is fine\n    c.commitSync();                                   // still on the owning thread\n}","handlingStrategy":"validation","validationCode":"// Pin the consumer to its creating thread; validate before every call\nThread owner = consumerOwnerThread; // captured at `new KafkaConsumer<>(...)`\nif (Thread.currentThread() != owner) {\n    throw new IllegalStateException(\n        \"KafkaConsumer touched from \" + Thread.currentThread().getName() +\n        \", expected \" + owner.getName());\n}\nconsumer.poll(Duration.ofSeconds(1));","typeGuard":"// Thread-affine wrapper: every delegated method asserts single-thread ownership\nstatic <K, V> Consumer<K, V> confineToCurrentThread(Consumer<K, V> c) {\n    final Thread owner = Thread.currentThread();\n    return new Consumer<K, V>() {\n        private void check() {\n            if (Thread.currentThread() != owner)\n                throw new IllegalStateException(\"consumer bound to \" + owner);\n        }\n        public ConsumerRecords<K, V> poll(Duration t) { check(); return c.poll(t); }\n        // ... delegate every other method through check() ...\n        public void wakeup() { c.wakeup(); } // the ONLY thread-safe method\n    };\n}","tryCatchPattern":"try {\n    consumer.poll(Duration.ofSeconds(1));\n} catch (ConcurrentModificationException e) {\n    // Do NOT retry from this thread; hand the work to the owning thread\n    // or replace with one-consumer-per-thread design.\n    throw new IllegalStateException(\"consumer misused across threads\", e);\n}","preventionTips":["Create the KafkaConsumer on the exact thread that will call poll/commit/seek on it.","Never share a consumer instance across threads; give each worker thread its own consumer.","For cross-thread signaling use consumer.wakeup() — it is the only thread-safe method.","Hand records off to other threads via a queue; do not let them touch the consumer."],"tags":["concurrency","consumer","threading"],"analyzedSha":"c31c9215e131f8c17e79f8901b48c13ee6aa8e7a","analyzedAt":"2026-08-03T12:34:05.770Z","schemaVersion":2}