Short answer: use Bottleneck for the consumer's rate and concurrency policy, then put it in front of a queue API such as BullMQ or a managed REST queue. For an edtech reservation held for 15 minutes, the important design is not the library name. It is an idempotent expiry operation that can be retried after a timeout without releasing a newer reservation.
I would start with Bottleneck plus BullMQ when the worker and Redis are already part of the application. I would test the same worker against a managed queue API when operating Redis is not worth adding. Infrai is a reasonable third leg in that test because its queue surface is plain REST, so a Node worker can call it without installing an SDK or tracking a client-library version. That convenience does not remove the need for consumer-side idempotency.
What should happen when a reservation expires?
The data flow is small. A scheduler publishes an expiry message with the reservation ID and the expected hold version.…