I've been overrating queueing services
Lots of our software needs queue-like behavior. So, for example:
- Suppose every new user needs to be registered in four subsystems. You might do the registration synchronously in one subsystem, enqueue the information necessary to do the rest, and let asynchronous workers process those events.
- More generally, suppose you process user requests that don't need to be handled synchronously. Those requests tend to come in bursts, so you might enqueue those requests and let workers handle them. Your workers will usually be provisioned so that processing is near-synchronous except after a burst, when the queue will take a while to drain.
- Suppose you'd like to offer a non-essential feature that requires expensive processing (e.g., if you're running a video hosting side and offering AI analysis of the videos). Queues are a popular solution: candidates for the expensive process are enqueued, and as you have time or budget, you pick candidates off the queue.
So I, like many of us, have spent a lot of time with SQS. (As usual, I'm using the AWS example, but other such services exist.) But the fact that queueing is a reasonable mental model of a situation doesn't entail that a queueing service is the right implementation tool. More and more, I'm using alternatives to SQS. There's no single formula that will always replace SQS, and I wouldn't always want to replace it, but here's a basic approach that works in many applications:
- In whatever database you're holding the information to be processed, ensure you have whatever information is necessary to "enqueue" it. This can vary, but is often some combination of a unique identifier, a timestamp, and a status field. Note that this part often comes for free (or requires a single extra index), because this information is already in one place.
- When a worker would take items from the queue, it instead reads items from the database.
- Other parts of the queueing system (recording worker results, retries, error-handling, and so on) work identically or analogously.
In most cases, this works like a queue and can be reasoned about, and monitored, like a queue. Meanwhile, you get some benefits:
- You don't need a dependency on the queueing service.
- If you need or want flexibility in how workers dequeue items, it's often much easier to implement a new dequeueing function with this approach than with a queueing service.
- You often get performance benefits.
Many people swear by SQS (or whatever other service). Here are attempts to address some potential objections. (Again, in some cases the objections really do mean you ought to be using SQS; I'm only claiming that it's unnecessary in most actual applications.)
- "With SQS, you can accept inputs from any number of sources." That's fair, and sometimes it is a reason to use SQS, but in most applications, (i) you're going to be gathering the relevant inputs in one place anyway and (ii) even if you aren't, it's as easy to pull the inputs from multiple sources as it is to push them from those sources.
- "We work with huge data, and SQS offers the queueing without requiring a huge database table." If your data are truly huge, then yes, this can be decisive. Most applications, though, do not operate over truly huge data.1
- "We work with a non-relational database that won't support queries like that." Do not underrate the power and performance of modern non-relational databases. Usually one DynamoDB secondary index will do the trick, and your data set needs to be pretty huge before that tends to cause any trouble.
- "SQS gives us a DLQ and other nice functionality for free." If you need exactly SQS's implementation of these things, then great. Very often, however, the DLQ abstraction (or whatever other SQS feature one has in mind) does not fit the application perfectly, and one needs to do work to adapt to it. Even more often, it's at least as easy to "redrive" failed events with the approach I described as with a DLQ exposed by a service.
- "You'll get all sorts of bugs with state management! Won't you need to mark some events as
pending? Won't you need to implement logic about when to releasependingevents? SQS does all of this for you." Again, SQS's abstractions might or might not fit your applications well. Many teams have to write some custom state-management code on top of SQS already, and the wickedest state-management bugs are vastly less likely to cause trouble unless you have tons of volume.
To oversimplify: if you're working at colossal scale or if you can really do your job by having having many semi-independent providers feeding events to a queue, then it can make a lot of sense to use SQS. In the modal case, however, I suspect you're better off manipulating a database table directly.
Some further notes:
- If I'm right that this is caused by our conflating a mental model with an implementation tool, this is yet another failure of encapsulation.
- Speaking of encapsulation: many programmers are intimidated by the prospect of implementing (e.g.) dequeueing logic daunting. This, like so many other operations, gets a lot easier when you have something like a repository layer to work with.
- This is a good example of generative AI indirectly affecting how I think about system design. Generative AI has made the migrations much easier, has made the replacement implementation faster and more reliable, and has increased the value of operating directly on services I control.2
- It seems to me that SQS and similar have, like Step Functions, acquired in many quarters a cachet as a "smart-person tool" or a sign of sophistication. It's easy enough to speculate about why SQS has gained this kind of respectability, but I'd love to read what historically- and sociologically-minded people have to say about this. Suffice to say that I've spent a lot of time advocating for alternatives to Step Functions, and in doing so have disproportionately often been perceived as unsophisticated.
- I wonder if, on some level, people conflate the indispensable, fundamental utility of the queue data structure with that of an instance of a SQS object (also called a "queue"). Perhaps it simply sounds silly to say "let's not use a queue here."