Per-Tenant Rate Limiting and Resource Quotas
HeliosDB Lite enforces per-tenant rate limits in SQL for shared SaaS databases: a role created with CREATE ROLE ... RATE_LIMIT receives a per-minute request budget, requests beyond it fail with a rate-limit error, and the helios_user_quotas view shows each role's limit and current usage, so a tenant mapped to its own role cannot use up the budget of the others.
Problem Being Solved
Core Problem Statement
Multi-tenant SaaS platforms cannot ensure fair resource allocation and SLA consistency when database access is uncontrolled, allowing single tenants with buggy code, malicious intent, or simply high-volume legitimate usage to consume disproportionate database capacity (CPU, I/O, connections, memory) and cause performance degradation for all other tenants.
Who Suffers Most
Product-Led Growth SaaS Companies: Self-service SaaS platforms (e.g., analytics, observability, developer tools) that allow customers to ingest arbitrary data volumes or run custom queries cannot safely enable high-tier features (large datasets, complex queries, high rate limits) for new customers without risking platform-wide degradation. This forces conservative default limits that hurt conversion ("your free tier is too restrictive") or requires manual account reviews that don't scale, directly limiting PLG velocity.
Concrete Examples with Code, Config & Architecture
Example 1: Tiered QPS Quotas - Free/Pro/Enterprise SaaS
HeliosProxy Configuration (helios-proxy-tiered.toml):
[proxy]
listen_address = "0.0.0.0:5432"
admin_listen_address = "127.0.0.1:9090"
mode = "transaction"
log_level = "info"
[backend]
host = "helios-db"
port = 5433
max_connections = 100
[tenant_quotas]
enabled = true
identifier = "application_name" # Extract tenant_id from connection param
default_tier = "free"
# Tenant tier configurations
[[tenant_quotas.tiers]]
name = "free"
max_qps = 100
max_connections = 10
max_transaction_duration = "10s"
storage_quota_gb = 10
burst_allowance = 1.5
enforcement_action = "throttle" # Options: throttle, reject, queue
[[tenant_quotas.tiers]]
name = "pro"
max_qps = 500
max_connections = 50
max_transaction_duration = "5m"
storage_quota_gb = 100
burst_allowance = 2.0
enforcement_action = "queue" # Queue excess requests up to 5s
[[tenant_quotas.tiers]]
name = "enterprise"
max_qps = 5000
max_connections = 500
max_transaction_duration = "1h"
storage_quota_gb = 1000
burst_allowance = 3.0
enforcement_action = "throttle" # Graceful degradation
# Assign tenants to tiers
[[tenant_quotas.assignments]]
tenant_pattern = "free_tenant_*"
tier = "free"
[[tenant_quotas.assignments]]
tenant_pattern = "pro_tenant_*"
tier = "pro"
[[tenant_quotas.assignments]]
tenant_pattern = "enterprise_*"
tier = "enterprise"
# Override for specific tenant (temporary rate increase)
[[tenant_quotas.overrides]]
tenant_id = "pro_tenant_acme_corp"
max_qps = 1500 # Temporarily increased for batch job
expires_at = "2025-12-20T00:00:00Z"
[quota_metrics]
enabled = true
prometheus_port = 9092
alert_on_quota_violation = true
alert_threshold = 10
Application Code (Node.js):
const { Pool } = require('pg');
// Tenant-aware connection pool
class TenantAwarePool {
constructor(tenantId, tenantTier) {
this.tenantId = tenantId;
this.tenantTier = tenantTier;
// Small per-service pool - HeliosProxy handles quotas
this.pool = new Pool({
host: 'helios-proxy',
port: 5432,
database: 'saas_db',
user: 'app_user',
password: process.env.DB_PASSWORD,
max: 5, // Small pool
application_name: `${tenantTier}_tenant_${tenantId}`, // Quota identifier
statement_timeout: this.getTierTimeout(tenantTier),
});
}
getTierTimeout(tier) {
const timeouts = { free: 10000, pro: 300000, enterprise: 3600000 };
return timeouts[tier] || 10000;
}
async query(sql, params) {
try {
const result = await this.pool.query(sql, params);
return result;
} catch (error) {
// HeliosProxy quota errors have specific error codes
if (error.code === 'HELIOS_QUOTA_EXCEEDED') {
console.error(`Quota exceeded for tenant ${this.tenantId}:`, error.message);
// Return user-friendly error
throw new Error(`Rate limit exceeded. Please upgrade to ${this.getNextTier()} tier.`);
}
throw error;
}
}
getNextTier() {
const tiers = { free: 'Pro', pro: 'Enterprise', enterprise: 'Custom' };
return tiers[this.tenantTier] || 'higher';
}
async close() {
await this.pool.end();
}
}
// Example API endpoint with quota-aware error handling
async function handleTenantQuery(req, res) {
const { tenantId, tenantTier } = req.auth; // From JWT/auth middleware
const tenantPool = new TenantAwarePool(tenantId, tenantTier);
try {
// This query counts toward tenant's QPS quota
const result = await tenantPool.query(
`SELECT id, name, value, created_at
FROM tenant_data
WHERE tenant_id = $1
ORDER BY created_at DESC
LIMIT $2`,
[tenantId, req.query.limit || 100]
);
res.json({
data: result.rows,
quota_status: {
tier: tenantTier,
// Can fetch from HeliosProxy admin API
}
});
} catch (error) {
if (error.message.includes('Rate limit exceeded')) {
res.status(429).json({
error: error.message,
retry_after: 1, // seconds
upgrade_url: '/pricing'
});
} else {
res.status(500).json({ error: 'Internal server error' });
}
} finally {
await tenantPool.close();
}
}
module.exports = { TenantAwarePool, handleTenantQuery };
Load Test Results (Simulate noisy neighbor):
Scenario: 100 tenants (80 free, 15 pro, 5 enterprise).
Example 2: Storage Quota Enforcement - Prevent Disk Exhaustion
Scenario: Prevent individual tenants from uploading unlimited data and filling shared storage.
HeliosProxy Configuration (helios-proxy-storage.toml):
[tenant_quotas.storage]
enabled = true
check_interval = "60s" # Update storage metrics every minute
enforcement = "block_writes" # Block INSERT/UPDATE when quota exceeded
warning_threshold = 0.9 # Warn at 90% usage
# Storage calculation query (customize per schema)
storage_query = """
SELECT
application_name AS tenant_id,
SUM(pg_total_relation_size(schemaname || '.' || tablename)) AS bytes_used
FROM pg_tables
WHERE schemaname = 'public'
GROUP BY application_name
"""
[[tenant_quotas.tiers]]
name = "free"
storage_quota_gb = 5
[[tenant_quotas.tiers]]
name = "pro"
storage_quota_gb = 50
[[tenant_quotas.tiers]]
name = "enterprise"
storage_quota_gb = 500
Example 3: Transaction Duration Limits - Prevent Lock Holding
Scenario: Prevent long-running transactions from holding locks and blocking other tenants.
HeliosProxy Configuration (helios-proxy-txn-limits.toml):
[tenant_quotas.transaction]
enabled = true
check_interval = "1s" # Check every second for long-running transactions
enforcement = "rollback" # Rollback transactions exceeding limit
[[tenant_quotas.tiers]]
name = "free"
max_transaction_duration = "10s"
max_idle_in_transaction = "5s"
[[tenant_quotas.tiers]]
name = "pro"
max_transaction_duration = "5m"
max_idle_in_transaction = "30s"
[[tenant_quotas.tiers]]
name = "enterprise"
max_transaction_duration = "1h"
max_idle_in_transaction = "10m"
Application Code (Java with transaction timeout awareness):
import java.sql.*;
import java.util.Properties;
public class TenantTransactionManager {
private String tenantId;
private String tier;
private Connection conn;
public TenantTransactionManager(String tenantId, String tier) throws SQLException {
this.tenantId = tenantId;
this.tier = tier;
Properties props = new Properties();
props.setProperty("user", "app_user");
props.setProperty("password", "secret");
props.setProperty("ApplicationName", tier + "_tenant_" + tenantId);
this.conn = DriverManager.getConnection(
"jdbc:postgresql://helios-proxy:5432/saas_db",
props
);
}
public void executeBatchOperation(List<BatchItem> items) throws SQLException {
// HeliosProxy will rollback if transaction exceeds tier limit
conn.setAutoCommit(false);
try (PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO batch_records (tenant_id, data) VALUES (?, ?)"
)) {
int count = 0;
for (BatchItem item : items) {
stmt.setString(1, tenantId);
stmt.setString(2, item.getData());
stmt.addBatch();
count++;
if (count % 1000 == 0) {
// Execute in chunks to stay under transaction time limit
stmt.executeBatch();
conn.commit();
System.out.println("Committed batch of " + count + " records");
// Start new transaction for next batch
conn.setAutoCommit(false);
}
}
// Commit remaining
stmt.executeBatch();
conn.commit();
} catch (SQLException e) {
conn.rollback();
if (e.getMessage().contains("HELIOS_TRANSACTION_TIMEOUT")) {
System.err.println(
"Transaction exceeded " + tier + " tier time limit. " +
"Consider upgrading or processing in smaller batches."
);
throw new TransactionTimeoutException(
"Transaction too long for " + tier + " tier", e
);
}
throw e;
} finally {
conn.setAutoCommit(true);
}
}
public static class TransactionTimeoutException extends SQLException {
public TransactionTimeoutException(String message, Throwable cause) {
super(message, cause);
}
}
}
Lock Contention Results:
Scenario: Free-tier tenant attempts 2-minute batch operation (20s limit), while 50 other tenants run queries.
Example 4: Dynamic Quota Adjustment API
Scenario: Allow tenants to request temporary quota increases for batch jobs or upgrades.
HeliosProxy Admin API Usage:
# Get current tenant quotas
curl -X GET http://helios-proxy:9090/api/v1/tenants/pro_tenant_acme/quotas
Response:
{
"tenant_id": "pro_tenant_acme",
"tier": "pro",
"quotas": {
"max_qps": 500,
"current_qps": 234,
"max_connections": 50,
"current_connections": 18,
"storage_quota_gb": 100,
"storage_used_gb": 45.3,
"max_transaction_duration": "5m"
},
"usage_stats": {
"qps_1h_avg": 210,
"qps_1h_p99": 420,
"quota_violations_24h": 0
}
}
# Temporarily increase quota for batch job
curl -X POST http://helios-proxy:9090/api/v1/tenants/pro_tenant_acme/quotas/override \
-H "Authorization: Bearer $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"max_qps": 2000,
"reason": "Monthly batch processing",
"expires_at": "2025-12-16T06:00:00Z"
}'
Response:
{
"success": true,
"tenant_id": "pro_tenant_acme",
"override_id": "ovr_1a2b3c4d",
"new_quotas": {
"max_qps": 2000,
"expires_at": "2025-12-16T06:00:00Z"
},
"message": "Quota override applied. Will revert in 8 hours."
}
# Upgrade tenant tier
curl -X PATCH http://helios-proxy:9090/api/v1/tenants/pro_tenant_acme/tier \
-H "Authorization: Bearer $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"tier": "enterprise"}'
Response:
{
"success": true,
"tenant_id": "pro_tenant_acme",
"old_tier": "pro",
"new_tier": "enterprise",
"new_quotas": {
"max_qps": 5000,
"max_connections": 500,
"storage_quota_gb": 1000,
"max_transaction_duration": "1h"
},
"applied_at": "2025-12-15T14:23:45Z"
}
Self-Service Quota Management (Customer Portal):
// React component for tenant self-service quota dashboard
import React, { useState, useEffect } from 'react';
import { LineChart, Line, XAxis, YAxis, Tooltip, Legend } from 'recharts';
interface QuotaStatus {
current: number;
limit: number;
percentage: number;
}
function QuotaDashboard({ tenantId }: { tenantId: string }) {
const [quotas, setQuotas] = useState<any>(null);
const [qpsHistory, setQpsHistory] = useState<any[]>([]);
useEffect(() => {
// Fetch from HeliosProxy admin API (proxied through backend)
fetch(`/api/tenants/${tenantId}/quotas`)
.then(res => res.json())
.then(data => {
setQuotas(data.quotas);
setQpsHistory(data.usage_history);
});
}, [tenantId]);
const getQuotaColor = (percentage: number) => {
if (percentage > 90) return 'red';
if (percentage > 75) return 'orange';
return 'green';
};
const handleRequestIncrease = async (quotaType: string) => {
const response = await fetch(`/api/tenants/${tenantId}/request-quota-increase`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ quota_type: quotaType })
});
// Handle response...
};
if (!quotas) return <div>Loading...</div>;
const qpsUsage = (quotas.current_qps / quotas.max_qps) * 100;
const storageUsage = (quotas.storage_used_gb / quotas.storage_quota_gb) * 100;
const connectionsUsage = (quotas.current_connections / quotas.max_connections) * 100;
return (
<div className="quota-dashboard">
<h2>Resource Usage - {quotas.tier} Tier</h2>
<div className="quota-cards">
<div className="quota-card">
<h3>Queries Per Second</h3>
<div className="quota-bar">
<div
className="quota-fill"
style={{
width: `${qpsUsage}%`,
backgroundColor: getQuotaColor(qpsUsage)
}}
/>
</div>
<p>
{quotas.current_qps} / {quotas.max_qps} QPS ({qpsUsage.toFixed(1)}%)
</p>
{qpsUsage > 75 && (
<button onClick={() => handleRequestIncrease('qps')}>
Request Increase
</button>
)}
</div>
<div className="quota-card">
<h3>Storage</h3>
<div className="quota-bar">
<div
className="quota-fill"
style={{
width: `${storageUsage}%`,
backgroundColor: getQuotaColor(storageUsage)
}}
/>
</div>
<p>
{quotas.storage_used_gb.toFixed(1)} / {quotas.storage_quota_gb} GB
({storageUsage.toFixed(1)}%)
</p>
</div>
<div className="quota-card">
<h3>Connections</h3>
<div className="quota-bar">
<div
className="quota-fill"
style={{
width: `${connectionsUsage}%`,
backgroundColor: getQuotaColor(connectionsUsage)
}}
/>
</div>
<p>
{quotas.current_connections} / {quotas.max_connections} connections
</p>
</div>
</div>
<div className="qps-chart">
<h3>QPS History (24 hours)</h3>
<LineChart width={600} height={300} data={qpsHistory}>
<XAxis dataKey="timestamp" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="qps" stroke="#8884d8" name="QPS" />
<Line type="monotone" dataKey="limit" stroke="#ff0000" name="Limit" strokeDasharray="5 5" />
</LineChart>
</div>
{quotas.tier !== 'enterprise' && (
<div className="upgrade-cta">
<h3>Need More Resources?</h3>
<p>Upgrade to {quotas.tier === 'free' ? 'Pro' : 'Enterprise'} for higher limits</p>
<button>View Pricing</button>
</div>
)}
</div>
);
}
export default QuotaDashboard;
Example 5: Multi-Cluster Quota Federation
Scenario: Global SaaS with databases in 3 regions needs unified per-tenant quotas.
Distributed HeliosProxy Setup:
# helios-proxy-us-east.toml (US East region)
[proxy]
listen_address = "0.0.0.0:5432"
region = "us-east-1"
[backend]
host = "helios-db-us-east"
port = 5433
[tenant_quotas]
enabled = true
mode = "federated" # Quota enforcement coordinated across regions
[tenant_quotas.federation]
coordination_backend = "redis" # Central quota state
redis_url = "redis://quota-coordinator.global:6379"
sync_interval = "100ms"
quota_allocation = "proportional" # Divide global quota across regions
# Global QPS quota of 5000 for tenant splits as:
Adoption Strategy
Phase 1: Proof of Concept (Weeks 1-4)
Objectives: Validate quota enforcement accuracy and identify top noisy-neighbor tenants.
Activities: 1. Week 1: Deploy HeliosProxy in observability mode (monitor only, no enforcement) in production. Instrument to collect per-tenant QPS, connection count, and query duration metrics. Run for 7 days to establish baseline.
Week 2: Analyze metrics to identify top 10 resource-intensive tenants. Calculate appropriate quotas for each tier (free/pro/enterprise) based on P95 usage patterns. Configure quota rules in staging environment.
Week 3: Enable enforcement mode in staging. Run load tests simulating quota violations. Validate that quotas are enforced without impacting compliant tenants. Test dynamic quota adjustment APIs.
Week 4: Conduct failure scenario testing: Redis coordinator failure (should degrade gracefully to local enforcement), proxy restart (should preserve quota state), tenant tier migration (should apply new quotas instantly). Document edge cases and rollback procedures.
Phase 2: Pilot Deployment (Weeks 5-12)
Objectives: Deploy quota enforcement for top-tier customers and known abusive tenants; measure incident reduction.
Activities: 1. Weeks 5-6: Enable quota enforcement for enterprise tier customers first (most important to protect). Monitor for 2 weeks.
Weeks 7-8: Expand enforcement to pro tier. Configure alerting for quota violations to proactively identify growth or issues.
Weeks 9-10: Implement storage quotas. Start with soft limits (warnings only) to understand growth patterns. Build customer-facing quota dashboard for transparency.
Weeks 11-12: Enable full enforcement for all tiers including free. Measure noisy-neighbor incident reduction, P99 latency variance across tenants, and operational burden reduction. Conduct post-pilot review.
Risk Mitigation: - Per-tenant rollout (easy to disable quotas for specific tenant if issues) - Generous initial limits (reduce later based on data) - 24/7 monitoring during enforcement rollout - Customer communication plan for free-tier users hitting limits
Phase 3: Full Rollout
Objectives: Optimize quotas based on pilot data; integrate quotas into pricing/billing; enable self-service.
Activities: 1. Weeks 13-16: Refine quota tiers based on usage data. Implement usage-based pricing model backed by accurate HeliosProxy metrics. Integrate quota status into customer billing dashboards.
Weeks 17-20: Enable customer self-service quota increase requests. Build internal tools for support team to grant temporary quota increases. Implement auto-scaling quota adjustments for seasonal traffic patterns.
Weeks 21-24: Extend quota enforcement to storage and transaction durations (currently focused on QPS/connections). Implement federated quotas for multi-region deployments. Build cost allocation/chargeback reporting for finance team.
Ongoing: Continuous optimization: machine learning-based anomaly detection for abusive patterns, predictive alerts when tenants approach limits (proactive upgrade outreach), quota recommendations for new tenant tiers, integration with pricing optimization experiments.
Long-Term Benefits: - Foundation for resource-based pricing that scales with customer value - Predictable infrastructure costs (no over-provisioning for peak usage) - Enterprise sales acceleration (can confidently offer performance SLAs) - Product differentiation (quota transparency builds trust)
Conclusion
Per-tenant resource quotas represent a fundamental requirement for sustainable multi-tenant SaaS operations, yet most platforms rely on reactive incident response rather than proactive enforcement due to the technical complexity of implementing database-native quotas.
The strategic value extends beyond incident prevention. Accurate per-tenant resource metering unlocks usage-based pricing models that align revenue with customer value, transforming quotas from a cost center into a revenue driver. Customer-facing quota dashboards build trust through transparency and create natural upgrade paths when tenants approach limits.
Performance depends on workload and hardware. Published figures are measured on representative fixtures; see the benchmarks page and reproduce them on your own workload.