Get 20% off web development packages
How I Built a SaaS System That Handles 100,000 Users
How I Built a SaaS System That Handles 100,000 Users
Professional Guide by Alaa Amer - Expert Web Developer & Designer
Scaling a SaaS product from 0 to 100,000 active users is a different challenge from simply building one. This is a real walkthrough of the decisions, tradeoffs, and architecture that made it possible.
2️⃣ Multi-Tenancy: One Database or Many?
The biggest SaaS architecture decision. I chose shared database with tenant isolation (most cost-effective at scale).
-- Every table has a tenant_id column
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Composite index for performance
CREATE INDEX idx_projects_tenant ON projects (tenant_id, created_at DESC);
-- Row-Level Security for safety
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON projects
USING (tenant_id = current_setting('app.current_tenant')::UUID);
Setting the tenant context per request:
// middleware/tenant.ts
export async function tenantMiddleware(req, res, next) {
const tenant = await resolveTenantFromHostname(req.hostname);
if (!tenant) return res.status(404).json({ error: "Tenant not found" });
// Set PostgreSQL session variable
await db.query(`SET LOCAL app.current_tenant = '${tenant.id}'`);
req.tenant = tenant;
next();
}
4️⃣ Caching Strategy with Redis
Cache aggressively — but cache the right things.
// lib/cache.ts
import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
// Generic cache wrapper
export async function cached<T>(
key: string,
ttlSeconds: number,
fetcher: () => Promise<T>,
): Promise<T> {
const hit = await redis.get(key);
if (hit) return JSON.parse(hit) as T;
const value = await fetcher();
await redis.setex(key, ttlSeconds, JSON.stringify(value));
return value;
}
// Cache invalidation by pattern
export async function invalidatePattern(pattern: string) {
const keys = await redis.keys(pattern);
if (keys.length > 0) await redis.del(...keys);
}
Usage in a route:
// api/projects.ts
app.get("/api/projects", async (req, res) => {
const { tenantId } = req.tenant;
const projects = await cached(
`tenant:${tenantId}:projects`,
60, // 60 seconds TTL
() =>
db.query(
"SELECT * FROM projects WHERE tenant_id = $1 ORDER BY created_at DESC",
[tenantId],
),
);
res.json(projects.rows);
});
Cache layers I used:
- L1: In-memory (node-cache) — sub-millisecond, per-process
- L2: Redis — shared across all instances, milliseconds
- L3: CDN edge cache — for fully public, static responses
6️⃣ Auth, Rate Limiting, and Security
// middleware/auth.ts
import jwt from "jsonwebtoken";
export function authenticate(req, res, next) {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(401).json({ error: "Unauthorized" });
try {
const payload = jwt.verify(token, process.env.JWT_SECRET) as JWTPayload;
req.user = payload;
next();
} catch {
return res.status(401).json({ error: "Invalid token" });
}
}
// Rate limiting per tenant
import rateLimit from "express-rate-limit";
import RedisStore from "rate-limit-redis";
export const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute window
max: 300, // 300 requests per minute per tenant
keyGenerator: (req) => req.tenant?.id ?? req.ip,
store: new RedisStore({ client: redis }),
handler: (req, res) => {
res.status(429).json({ error: "Rate limit exceeded. Please slow down." });
},
});
Summary
In this article we covered:
✅ Foundation and technology stack decisions ✅ Multi-tenancy with PostgreSQL Row-Level Security ✅ Database indexing and connection pooling ✅ Multi-layer caching with Redis ✅ Background job queues with Bull ✅ Auth, rate limiting, and security patterns ✅ Complete production-ready SaaS server skeleton
The biggest lesson: performance problems at scale are almost always solvable — but only if your architecture doesn't fight you. Build the right foundation early.
📩 Need help architecting your SaaS product?
Article Category
How I Built a SaaS System That Handles 100,000 Users
A real-world walkthrough of building a scalable SaaS architecture: multi-tenancy, database design, caching, queues, auth, monitoring, and lessons learned.
Consultation & Communication
Direct communication via WhatsApp or phone to understand your project needs precisely.
Planning & Scheduling
Creating clear work plan with specific timeline for each project phase.
Development & Coding
Building projects with latest technologies ensuring high performance and security.
Testing & Delivery
Comprehensive testing and thorough review before final project delivery.
Services Related to This Article
All ServicesWant to apply this article to your project?
If this topic is relevant to your current project, you can jump to one of the services above or browse the services page to choose the most suitable solution.