queue.SimpleQueue is a thread-safe FIFO queue for passing tasks or data between threads. It is designed for situations where you do not need a capacity limit, explicit task tracking, or manual lock management. Its interface is intentionally small: producers add objects with put(), and consumers remove them with get().
This makes SimpleQueue useful for worker pipelines, background processing, event collection, asynchronous logging, and communication between concurrent components inside one Python process.
What FIFO means
FIFO means first in, first out. The first object inserted is the first object returned.
from queue import SimpleQueue
queue = SimpleQueue()
queue.put("first")
queue.put("second")
print(queue.get())
print(queue.get())
The output is first and then second. Besides preserving order, the queue synchronizes access when multiple threads use it at the same time.
SimpleQueue versus Queue
queue.Queue includes a maximum capacity, task_done(), and join(). SimpleQueue is unbounded and does not track whether retrieved tasks have finished. Its smaller feature set is appropriate when all you need is a safe channel between threads.
Choose Queue when you need backpressure, bounded memory, or completion tracking. Choose SimpleQueue when you only need straightforward thread-safe transfer.
The producer-consumer pattern
A common design uses one thread to create work and another to process it.
from queue import SimpleQueue
from threading import Thread
queue = SimpleQueue()
def producer():
for number in range(5):
queue.put(number)
queue.put(None)
def consumer():
while True:
item = queue.get()
if item is None:
break
print(item * 2)
Thread(target=producer).start()
Thread(target=consumer).start()
Here, None acts as a sentinel indicating that no more items will arrive. In production code, a unique object is safer because None might be valid input.
STOP = object()
Blocking behavior
By default, get() blocks until an item is available. This avoids busy loops that repeatedly check the queue and waste CPU time.
You can use get_nowait() when the thread must continue doing other work. If no item exists, Python raises queue.Empty.
from queue import Empty, SimpleQueue
queue = SimpleQueue()
try:
item = queue.get_nowait()
except Empty:
print("no item is available")
Do not use empty as synchronization
empty() reports only a temporary observation. Another thread can add or remove an item immediately after the check. Therefore, it cannot guarantee that the next get() will not block.
When using non-blocking access, catch Empty. Concurrent state can change between any two instructions.
Threads, not processes
SimpleQueue is intended for threads inside the same process. It is not a replacement for multiprocessing.Queue when producers and consumers live in different processes.
For related control-flow concepts, see the guide to Python asyncio.Runner. Resource-heavy workers should also use predictable cleanup strategies, as discussed in Python weakref.finalize cleanup.
Event processing
A simple queue can separate event creation from slower processing.
from queue import SimpleQueue
from threading import Thread
EVENTS = SimpleQueue()
STOP = object()
def event_worker():
while True:
event = EVENTS.get()
if event is STOP:
return
print(f"event: {event}")
worker = Thread(target=event_worker)
worker.start()
EVENTS.put({"type": "login", "user": 42})
EVENTS.put(STOP)
worker.join()
The producer returns quickly, while the consumer can persist, batch, validate, or transmit events.
Handling consumer exceptions
An unhandled exception can terminate a worker and leave queued objects unprocessed. Wrap task execution in a controlled exception boundary.
def safe_consumer():
while True:
item = queue.get()
if item is STOP:
break
try:
process(item)
except Exception as error:
record_failure(item, error)
Decide whether failed items should be discarded, retried, or sent to a separate error queue. Unlimited retries can create an endless loop, so production systems should include attempt limits.
Memory and backpressure
SimpleQueue has no maximum capacity. If producers consistently run faster than consumers, memory usage can continue growing. This is the most important operational limitation.
Use queue.Queue(maxsize=N) when input volume is unpredictable. The capacity limit causes producers to wait and creates backpressure. When work can be grouped, the article about Python itertools.batched explains how batching can reduce repeated calls.
Multiple consumers
Several threads can call get() on one queue. Each item is removed by one consumer. The exact distribution depends on scheduling, so do not assume round-robin behavior or task affinity.
workers = [Thread(target=safe_consumer) for _ in range(4)]
for worker in workers:
worker.start()
When shutting down, add one sentinel for every worker. A single sentinel stops only the thread that receives it.
Mutable objects
The queue transfers object references. It does not copy objects. If a producer changes a mutable object after inserting it, the consumer may observe unexpected state. Prefer immutable values, snapshots, dataclasses treated as immutable, or copies created before calling put().
When not to use SimpleQueue
Do not use it for communication between machines, durable delivery, persistence across process restarts, or recovery after a crash. Systems that require those guarantees should use a message broker or durable stream such as RabbitMQ, Redis Streams, Kafka, or a managed queue.
It is also not a drop-in replacement for asyncio.Queue. Calling blocking get() from an event-loop thread can freeze asynchronous execution.
Testing queue-based workers
Test FIFO order, sentinel shutdown, concurrent production, exception handling, and clean termination. Avoid tests based only on arbitrary sleep() calls because they can be slow and unreliable. Prefer events, joins, and controlled timeouts.
The guide to Python StrEnum provides a useful way to represent closed worker states such as waiting, running, failed, and stopped.
Operational monitoring
Because the queue is unbounded, monitor production rate, consumption rate, processing latency, worker failures, and memory use. qsize() can be useful as an approximate metric, but it should not drive correctness decisions because concurrent changes can occur immediately.
Best practices
Use a unique sentinel, document who owns shutdown, catch task exceptions, stop threads with join(), keep queued values small, avoid modifying objects after insertion, and choose bounded Queue when memory pressure must be controlled. Keep the worker protocol explicit so maintainers know whether retries and error queues exist.
References
Read the official queue.SimpleQueue documentation and the official threading documentation.
Conclusion
queue.SimpleQueue is a focused solution for FIFO communication between threads. It handles synchronization and offers a small, readable interface. It is most effective when no capacity limit or completion counter is required. Its unbounded nature remains the key design consideration: measure the balance between producers and consumers, and switch to Queue(maxsize) whenever backpressure is necessary.







