Your job queue is a database table until it is not
A predictable moment in a young project: something needs to happen outside the request cycle — send an email, generate a report, process an upload — and the immediate instinct is to add a queue. Redis, or RabbitMQ, or Kafka if someone has recently read about Kafka.
For a great many projects, a table in the database you already run will do the job for years.
What that looks like
A table with a payload, a status, an attempt count and a scheduled time. A worker process that claims rows atomically, does the work, and marks them done or failed. Retries are an update. The dead letter queue is a status value. Inspecting the queue is a SELECT, which means debugging it requires no new tools and no new access.
Crucially, enqueuing a job participates in the same transaction as the work that caused it. If the order fails to save, the confirmation email is not queued, because both were in the same transaction. Getting that guarantee with an external broker requires the outbox pattern, which is more moving parts than most teams expect.
When you have actually outgrown it
There are real signals. Sustained throughput where queue polling contends meaningfully with your application's normal queries. Multiple independent consumers needing the same stream of events. Genuine fan-out, where one event triggers many unrelated downstream reactions. Retention requirements, where you need to replay a week of events into a new consumer.
That last one is the strongest signal for something like Kafka, and it is the one people most often do not have. LinkedIn built Kafka because they had enormous streams of activity data that many different systems needed to consume independently, at different speeds, with the ability to rewind. If you have one producer and one consumer, you have bought a distributed log to do a to-do list's job.
The general shape
Every piece of infrastructure has an operational cost that is easy to underestimate at adoption and impossible to ignore at 3am. The version that uses something you already run and understand is usually correct until you can name the specific property it lacks.