Skip to content

[Bug] dashboard-api: INCR and EXPIRE in dbExport are not atomic — export quota key can persist without TTL if process crashes #258

Description

@anshul23102

Describe the bug

In apps/dashboard-api/src/controllers/dbExport.controller.js, the daily export quota key is incremented and then given a TTL in two separate Redis operations:

const newCount = await redis.incr(key);
if (newCount === 1) {
    await redis.expire(key, 86400);
}

If the Node.js process crashes, is restarted, or loses the Redis connection between the INCR and the EXPIRE calls, the key is created but never given an expiry. The counter for that project/day persists indefinitely, permanently blocking the project from making further export requests on that day — or preventing exports forever if the TTL is never applied.

To Reproduce

  1. Send an export request that creates the counter key for the first time (newCount === 1).
  2. Simulate a crash between INCR and EXPIRE (e.g., kill -9 the process, or introduce a mock that throws before EXPIRE).
  3. Restart the server.
  4. Send export requests. The key still exists with value = 1 and no TTL, so the limit check passes normally — but if you kill before EXPIRE on every first request, the key has no TTL.
  5. Over multiple days the counter accumulates without resetting.

Expected behavior

The increment and TTL assignment should be atomic. Use SET key 1 EX 86400 NX for the first write, then INCR for subsequent ones. Or use the incrWithTtlAtomic helper already present in @urbackend/common:

const newCount = await incrWithTtlAtomic(redis, key, 86400, 1);
if (newCount > maxExports) {
    await redis.decr(key);
    return next(new AppError(429, `Daily export limit reached...`));
}

Additional context

File: apps/dashboard-api/src/controllers/dbExport.controller.js

Labels: bug, nsoc26, level2

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions