cloudflare-workers

Decrementing a Count Property by 1 with Cloudflare Workers KV and Promise.all causes count to get lost


I'm using workers KV to keep track of # of credits a user has (the count property).

i.e. {user1234: {credits: 10}}

In my client application, I'm using Promise.all to make multiple requests to an endpoint on my Worker /decrementCreditsByOne which does what the name implies.

It works great locally, but on the deployed worker, The count loses track.

My app frequently uses Promise.all to call /decrementCreditsByOne at least 10 times.

the function essentially gets the JSON from KV using the unique key, decrement the credits by 1, then puts the value back in with the updated credits.

Is KV the wrong choice here? Would Durable Objects, or D1 be better? What would you recommend? Should this work and my logic is just wrong?

Also, I noticed timestamps aren't really a thing with KV. They aren't a requirement for my app but would be nice to have!

Answer from ChatGPT recommends DO or D1 but I'd love to hear from a human too!

https://chat.openai.com/share/68757cf5-eafb-4248-80d8-28d28b5bfefc


Solution

  • Yes, KV is not the right fit here. KV does not provide any way to "atomically" update a value. Your worker presumably reads the KV value, modifies it, and then writes it back. However, if two requests run in parallel, it's possible that both requests will read the value before either request writes the value. Thus, they may both read the same value of the counter (say, 5). The both of them subtract one, and write that value back. So they both write 4. So, it appears that one of the two decrements never happened. In fact, they both happened, but they both started from the same starting value.

    In addition to this problem, Workers KV is an "eventually consistent" datastore. That means, even if you know that the two requests did not run in parallel -- one started after the other finished -- it's still possible that the second request sees an old value. It can take some time (several seconds, maybe a minute) before everyone sees the updated value. Hence, Workers KV is not appropriate for storing values that are frequently modified.

    Durable Objects are a much better solution here. With Durable Objects, you would create an object owning each counter. All requests for that counter would be handled by the same object, which means they all go to the same machine, making synchronization easy. Within a Durable Object, the system automatically ensures that while one request is reading or writing storage, other requests are held back, so they cannot create confusion by running in parallel. Moreover, the storage itself is strongly consistent; it never returns an old value.

    You could also use D1 here, as long as you are able to design a single D1 request that decrements and returns the value. If you try to read and write the value in two separate requests, you'll have the same problem as with KV. I would recommend using Durable Objects rather than D1 for this use case.