> but the state is not consistent in a

but the state is not consistent in a concurrent requests
Can you give more details what exactly are you doing here? A Durable Object runs a single request at a specific point in time, and only schedules a different request when you do async IO in your logic. So as long as you are only doing synchronous operations, or one of the transaction() methods on the storage API that allow you to use async actions, your storage access should be consistent.
10 Replies
Bilal
Bilal9mo ago
I'm building a rate limiting worker that keeps a counter and reset it after a specified interval. I'm using DO state for maintaining the counter data and alarm to persist the state every 1s to storage
lambrospetrou
lambrospetrouOP9mo ago
OK, and what's the inconsistency you observed? What storage operations are you doing, can you share that snippet of code to see exactly where there might be something problematic?
Bilal
Bilal9mo ago
Sure, here is my DO object
import { DurableObject } from "cloudflare:workers";
import dayjs from "dayjs";
import { parseInterval } from "./utils";

export class RateLimiterObject extends DurableObject {

constructor(ctx, state) {
super(ctx, state);

ctx.blockConcurrencyWhile(async () => {
this.data = await ctx.storage.get('data');
});
}

setRateLimit(limit, interval) {
let { intervalTime, intervalUnit } = parseInterval(interval);
this.data = {
limit: parseInt(limit),
interval: interval,
count: 1,
resetsAt: dayjs().add(intervalTime, intervalUnit).format(),
};
}

async updateCount() {
this.data.count++;
let currentAlarm = await this.ctx.storage.getAlarm();
if (!currentAlarm) {
this.ctx.storage.setAlarm(1000);
}
}

getData() {
return this.data;
}

async alarm() {
if (this.data) {
if (dayjs(this.data.resetsAt).isBefore(dayjs())) {
await this.ctx.storage.delete('data');
} else {
await this.ctx.storage.put('data', this.data);
}
}

this.ctx.storage.setAlarm(1000);
}
}
import { DurableObject } from "cloudflare:workers";
import dayjs from "dayjs";
import { parseInterval } from "./utils";

export class RateLimiterObject extends DurableObject {

constructor(ctx, state) {
super(ctx, state);

ctx.blockConcurrencyWhile(async () => {
this.data = await ctx.storage.get('data');
});
}

setRateLimit(limit, interval) {
let { intervalTime, intervalUnit } = parseInterval(interval);
this.data = {
limit: parseInt(limit),
interval: interval,
count: 1,
resetsAt: dayjs().add(intervalTime, intervalUnit).format(),
};
}

async updateCount() {
this.data.count++;
let currentAlarm = await this.ctx.storage.getAlarm();
if (!currentAlarm) {
this.ctx.storage.setAlarm(1000);
}
}

getData() {
return this.data;
}

async alarm() {
if (this.data) {
if (dayjs(this.data.resetsAt).isBefore(dayjs())) {
await this.ctx.storage.delete('data');
} else {
await this.ctx.storage.put('data', this.data);
}
}

this.ctx.storage.setAlarm(1000);
}
}
lambrospetrou
lambrospetrouOP9mo ago
The setAlarm() needs the timestamp epoch millis when to run your handler, not the duration from "now". See https://developers.cloudflare.com/durable-objects/api/alarms/#setalarm You are always setting the alarm 1s after UNIX epoch (1970). Also, keep in mind that since you only store within the alarm handler, if your DO crashes for any reason before the alarm runs, the in memory state is lost. So, depending on your rate limit accuracy requirements, you should adjust the alarm frequency as necessary.
Bilal
Bilal9mo ago
oh great! Thank you for the feedback. I will update the alarm
lambrospetrou
lambrospetrouOP9mo ago
Also, you need to make sure that your this.data is initialized before trying to call updateCount() since if there is nothing stored yet, it will be undefined and you will try to increment the count, hence failing.
Bilal
Bilal9mo ago
would it help if I persist data on every this.updateCount invocation?
Also, you need to make sure that your this.data is initialized before trying to call updateCount()
oh interesting, what's the right way to ensure that?
lambrospetrou
lambrospetrouOP9mo ago
Up to your needs. You will always have consistent state but lead to more writes, so it depends on your needs. check that this.data is valid before trying to increase the count and initialize it if not. Or guarantee in the caller code that you never call updateCount() before setRateLimit() has been called, and persisted at least once. BTW, calling setRateLimit() multiple times means you reset the counter each time. I don't know how you use the above DO so check your business logic needs.
Bilal
Bilal9mo ago
sounds good. let me work on the feedback and report back. Thank you
Ashley
Ashley9mo ago
Not sure if it’s useful for your use case or if you have a need to use a DO, but Workers have a Rate Limiting API that would save you a lot of work if there’s nothing out of the ordinary that you need: https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/
Cloudflare Docs
Rate Limiting · Cloudflare Workers docs
Define rate limits and interact with them directly from your Cloudflare Worker

Did you find this page helpful?