Api Rate Abuse in Nestjs with Cockroachdb
Api Rate Abuse in Nestjs with Cockroachdb — how this specific combination creates or exposes the vulnerability
Rate abuse in a NestJS application backed by CockroachDB arises from the interaction between application-layer throttling, database-side concurrency controls, and the operational characteristics of a distributed SQL store. Without explicit enforcement, an attacker can issue a high volume of requests that drive excessive read or write load on CockroachDB, trigger long-running or poorly indexed queries, and amplify the impact of missing or weak authentication/authorization checks.
NestJS does not enforce rate limits by default; if you add a global or controller-level guard, it typically tracks requests in memory or via an external store. CockroachDB provides serializable isolation and strong consistency, which prevents lost updates but does not prevent an attacker from saturating connection pools, driving high QPS, or causing contention on hotspots (for example, a row that many requests attempt to increment). If authentication is absent or bypassed (BOLA/IDOR), a rate limit that applies only to authenticated users may not protect unauthenticated endpoints, allowing enumeration or write actions to proceed unchecked.
The database can become a bottleneck under abusive load due to contention on primary keys or indexes, long-running transactions caused by inefficient queries, and repeated schema or metadata operations (such as upserts that perform read-before-write without optimistic checks). CockroachDB’s distributed nature means retries from the client or driver can increase load across nodes, especially when idempotency keys or request deduplication are not implemented. Without a per-user or per-IP sliding window combined with circuit-breaker patterns, a single compromised account or bot can generate enough transactions to degrade performance, increase latency, and affect availability.
Operational logging and metrics from NestJS (e.g., request timestamps, user identifiers, endpoint paths) combined with CockroachDB’s transaction telemetry can reveal patterns of abuse, such as repeated failed logins, high row contention on a tenant_id column, or bursts of writes to a shared resource. If rate limiting is not coordinated with the database’s transaction retry logic, clients may interpret transient errors as server failures and retry aggressively, compounding the load. Therefore, effective mitigation requires both application-side throttling and database-aware defenses such as request deduplication, statement timeouts, and well-designed indexes to minimize contention.
Cockroachdb-Specific Remediation in Nestjs — concrete code fixes
Implement rate limiting in NestJS using a token-bucket or sliding-window algorithm stored in a fast, external store (such as Redis), and coordinate with CockroachDB through short, targeted transactions and proper isolation levels. Always parameterize queries, enforce timeouts, and use optimistic concurrency control to avoid long-held locks and excessive retries.
- Example: NestJS guard with Redis-backed rate limiting
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { RedisService } from './redis.service';
@Injectable()
export class RateLimitGuard implements CanActivate {
constructor(private readonly redis: RedisService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const ip = request.ip;
const key = `rl:${ip}:${request.path}`;
const limit = 100; // requests
const windowSec = 60; // per minute
const current = await this.redis.incr(key);
if (current === 1) {
await this.redis.expire(key, windowSec);
}
if (current > limit) {
throw new HttpException('Too many requests', 429);
}
return true;
}
}
- Example: Parameterized query with timeout and optimistic concurrency in a NestJS service using CockroachDB node driver
import { Injectable } from '@nestjs/common';
import {
Cluster,
Database,
types,
} from 'cockroachdb';
@Injectable()
export class AccountsService {
private db: Database;
constructor() {
this.db = new Cluster({
connectionString: process.env.COCKRACKDB_URI,
// keep queries short and enforce timeouts at the driver level
maxIdleConns: 10,
maxOpenConns: 50,
}).db();
}
async transferMoney(fromId: string, toId: string, amount: number, requestId: string): Promise<void> {
const client = await this.db.connect();
try {
await client.query('SET application_name = $1', [`transfer-${requestId}`]);
await client.query('SET statement_timeout = $1::INTERVAL', ['15s']);
// Use serializable isolation (CockroachDB default) and explicit optimistic check
await client.query('BEGIN');
const res = await client.query(
'SELECT balance, version FROM accounts WHERE id = $1 FOR UPDATE',
[fromId]
);
if (res.rows.length === 0) throw new Error('Account not found');
const current = res.rows[0];
if (current.balance < amount) {
await client.query('ROLLBACK');
throw new Error('Insufficient balance');
}
// Conditional update using version for optimistic concurrency
const upd = await client.query(
'UPDATE accounts SET balance = balance - $1, version = version + 1 WHERE id = $2 AND version = $3 RETURNING version',
[amount, fromId, current.version]
);
if (upd.rowCount === 0) {
await client.query('ROLLBACK');
throw new Error('Concurrent modification, please retry');
}
await client.query(
'UPDATE accounts SET balance = balance + $1, version = version + 1 WHERE id = $2',
[amount, toId]
);
await client.query('COMMIT');
} finally {
client.release();
}
}
}
- Example: Idempotency key to deduplicate requests and reduce repeated writes to CockroachDB
import { Injectable } from '@nestjs/common';
import { Database } from 'cockroachdb';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class PaymentsService {
constructor(private readonly db: Database) {}
async processPayment(userId: string, amount: number, idempotencyKey?: string): Promise<string> {
const key = idempotencyKey || uuidv4();
// Short transaction: check then insert with unique constraint on idempotency_key
const client = await this.db.connect();
try {
await client.query('BEGIN');
const res = await client.query(
'SELECT status FROM idempotency_keys WHERE key = $1 FOR UPDATE',
[key]
);
if (res.rows.length > 0) {
await client.query('COMMIT');
return res.rows[0].status;
}
// Perform business logic, e.g., create a payment record
await client.query(
'INSERT INTO payments (id, user_id, amount) VALUES ($1, $2, $3)',
[uuidv4(), userId, amount]
);
await client.query(
'INSERT INTO idempotency_keys (key, status) VALUES ($1, $2)',
[key, 'completed']
);
await client.query('COMMIT');
return 'completed';
} finally {
client.release();
}
}
}
- Indexing and query hygiene to reduce contention and avoid full table scans that amplify load under abuse
-- Ensure tenant_id and created_at are indexed for time-bound queries
CREATE INDEX idx_accounts_tenant_created ON accounts (tenant_id, created_at);
-- Use covering index for common read patterns to avoid heap fetches under high QPS
CREATE INDEX idx_transactions_covering ON transactions (user_id, created_at) INCLUDE (amount, status);