How I Built a SaaS System That Handles 100,000 Users

We offer a comprehensive collection of essential educational articles in web development to turn your ideas into digital reality

How I Built a SaaS System That Handles 100,000 Users

Technology 2026-03-21 Alaa Amer

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 decisionsMulti-tenancy with PostgreSQL Row-Level SecurityDatabase indexing and connection poolingMulti-layer caching with RedisBackground job queues with BullAuth, rate limiting, and security patternsComplete 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?

SaaS Architecture Scalability Multi-Tenancy Redis Queues Backend DevOps
Article Category
Technology

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.

How I Built a SaaS System That Handles 100,000 Users
01

Consultation & Communication

Direct communication via WhatsApp or phone to understand your project needs precisely.

02

Planning & Scheduling

Creating clear work plan with specific timeline for each project phase.

03

Development & Coding

Building projects with latest technologies ensuring high performance and security.

04

Testing & Delivery

Comprehensive testing and thorough review before final project delivery.

Alaa Amer
Alaa Amer

Professional web developer with over 10 years of experience in building innovative digital solutions.

Need This Service?

Contact me now for a free consultation and quote

WhatsApp Your satisfaction is our ultimate goal

What We Offer

  • Website Maintenance & Updates

    Keep your website secure updated optimized

  • API Integration

    Connect your systems with powerful APIs

  • Database Design & Optimization

    Faster queries cleaner structure fewer issues

  • Website Security Hardening

    Protect your site from cyber threats

  • Automation & Scripts

    Automate repetitive tasks and save time

Have Questions?

Call Us Now

00201014714795