Hosna Qasmei
Hosna Qasmei•6mo ago

Trying to add rate limiting to newsletter sign up

I have this code, I'm trying to implement rate limiting so I don't get DDOS and to learn how to use it. If a user isn't logged in, and I want to limit based on IP address how would i do that? Also with the convex-helpers @ian , how does it know to increment? Does it just find the item with the same key? How do I get the key to be the IP address? Thank you
import { defineRateLimits } from 'convex-helpers/server/rateLimit';
import { ConvexError, v } from 'convex/values';

import { mutation } from './_generated/server';

const SECOND = 1000; // ms
const MINUTE = 60 * SECOND;
const HOUR = 60 * MINUTE;

const { rateLimit, checkRateLimit, resetRateLimit } = defineRateLimits({
newsletterSignUpRateLimit: { kind: 'fixed window', rate: 3, period: HOUR },
});

export const addEmail = mutation({
args: {
email: v.string(),
subscriptionDate: v.string(),
isActive: v.boolean(),
},
handler: async (ctx, args) => {
// Make sure the rate limit is not exceeded
// const { ok, retryAt } = await rateLimit(ctx, {
// name: 'newsletterSignUpRateLimit',
// });
// if (!ok) return { retryAt };

// Check if email already exists, if so, return error, else insert
const existingEmail = await ctx.db
.query('newsletters')
.withIndex('by_email', (q) => q.eq('email', args.email))
.first();

if (existingEmail) {
throw new ConvexError('Email already exists');
}

return await ctx.db.insert('newsletters', {
email: args.email,
subscriptionDate: args.subscriptionDate,
isActive: args.isActive,
});
},
});
import { defineRateLimits } from 'convex-helpers/server/rateLimit';
import { ConvexError, v } from 'convex/values';

import { mutation } from './_generated/server';

const SECOND = 1000; // ms
const MINUTE = 60 * SECOND;
const HOUR = 60 * MINUTE;

const { rateLimit, checkRateLimit, resetRateLimit } = defineRateLimits({
newsletterSignUpRateLimit: { kind: 'fixed window', rate: 3, period: HOUR },
});

export const addEmail = mutation({
args: {
email: v.string(),
subscriptionDate: v.string(),
isActive: v.boolean(),
},
handler: async (ctx, args) => {
// Make sure the rate limit is not exceeded
// const { ok, retryAt } = await rateLimit(ctx, {
// name: 'newsletterSignUpRateLimit',
// });
// if (!ok) return { retryAt };

// Check if email already exists, if so, return error, else insert
const existingEmail = await ctx.db
.query('newsletters')
.withIndex('by_email', (q) => q.eq('email', args.email))
.first();

if (existingEmail) {
throw new ConvexError('Email already exists');
}

return await ctx.db.insert('newsletters', {
email: args.email,
subscriptionDate: args.subscriptionDate,
isActive: args.isActive,
});
},
});
3 Replies
ian
ian•6mo ago
We don't expose the IP (and unfortunately IPs are a bit problematic due to ISPs). If you don't specify a user-specific key then it just applies a global rate limit - which would cap overall newsletter sign ups. Usually for logged-in users the key would be the logged in userId. Technically in the case of DDOS every IP would be different (that's what makes it a Distributed denial of service attack). If you're worried about a flood, then an overall limit would probably suffice. But that would serialize all sign-ups, so you'd run into database contention somewhere in the 10 QPS to 100 QPS range. If you want to prevent bots, then you can either put it behind a login, use a captcha, or use an "anonymous" user from convex-auth - and use the session ID as the key. For most "bots" they wouldn't take the time to be sophisticated enough to go through that flow over & over to subscribe.
Hosna Qasmei
Hosna QasmeiOP•6mo ago
Hey @ian , I appreciate your response. Yup did that method instead using https://www.google.com/recaptcha/ @Web Dev Cody clarified it for me 😅 The same reasoning you mentioned, still vulnerable to DDOSing if I did it the rate limiting way. Thank you!
reCAPTCHA
reCAPTCHA
reCAPTCHA is a security service that protects your websites from fraud and abuse.
ian
ian•6mo ago
I'll clarify that if there was a true DDOS (many users all hammering your service at once) - rate limiting would prevent signups. It has the property that it "fails closed" - when overwhelmed it fails (due to database contention), rather than letting more through (which upstash would do, e.g.). Before it fails, if you had a ton of requests that exceeded the rate limit, they'd just throw - there aren't any DB writes associated with being over the rate limit, only when you consume resources. If you exceeded the database ability to keep up with writes, you'd get OCC errors that signify that the writes are all competing. These also would show up as exceptions, preventing signups to the newsletter. The main limitation here is that during such an attack, normal users would also be unable to sign up for the newsletter. With a Captcha, a legit user could sign up any time. Note: with Captcha you need to do the verification in an action which then calls a mutation, vs. being able to call a mutation directly, since the callback requires making a fetch call to their servers

Did you find this page helpful?