Whenever someone says “this doesn’t scale,” it’s worth being skeptical. In practice, there’s almost always a specific, nameable bottleneck behind that sentence — misused caching, a missing lock, random I/O. It’s rarely the technology itself.

This is a short framework for turning a generic statement into a concrete investigation: identify the bottleneck and understand the trade-off of each solution.

1. Investigate before adding a cache

Caching hides the symptom, it doesn’t fix the root cause of bad performance. Sometimes it just postpones the problem: a cache miss, a bad invalidation, stale data served to the wrong user. That doesn’t mean caching is bad — it means it doesn’t replace actually understanding why the application is slow before throwing a cache layer on top.

Before getting there, it’s worth exhausting the basics in PHP: a properly configured opcache and a well-tuned FastCGI setup already solve most of what people try to fix with caching.

2. Put database scale in context

A real case: DigitalOcean ran a queue inside their own database (MySQL) for almost five years, from 2012 to 2017. Only after the business grew 10,000%, with more than 15,000 concurrent connections, did the architecture actually start to bottleneck — only then did they redesign the queue’s consumer services and move everything to RabbitMQ.

In other words: the relational database held up under considerable scale before another piece was needed in the architecture. Next time someone says “relational databases don’t scale” without giving context — what scale, what bottleneck, what trade-off — it’s worth being skeptical.

One practical tool that comes up in this kind of discussion is locking. Writing more than one piece of data at the same time can, and will, cause a race condition. In those cases, the less optimistic path is to lock the data while it’s in use — even if it costs performance. That’s not a design mistake, it’s a conscious trade-off: you’re paying latency to guarantee consistency, and that only makes sense when the data actually needs that guarantee.

3. Choose the queue for the problem

Kafka was built to move light data — small messages — with speed as the central design goal. Two strategies do most of the work:

  • Zero-copy read: avoids unnecessary data copies between kernel and user space when serving a read.
  • Sequential I/O: writes and reads disk sequentially instead of randomly, which is orders of magnitude faster on traditional disks — and still helps on SSDs.

That doesn’t make Kafka “better” than RabbitMQ. The choice between the two isn’t about which is faster, it’s about which guarantee, replay, and ordering model your problem actually needs. RabbitMQ with routing, exchanges, and dead-letter queues is still the “classic” queue — and understanding what’s underneath it helps anyone who already uses queues day to day and wants to know the reasoning behind the framework’s choices.

4. End with a better question

Next time “this doesn’t scale” comes up in a conversation, swap the phrase for a better question: what’s the specific bottleneck, and what’s the trade-off of fixing it now? Caching, databases, and queues are rarely the actual problem — the problem is not being able to name what’s actually weighing things down.