qqq Performance: Benchmarks and Best Practices
We share performance benchmarks from production deployments and best practices for optimizing your qqq applications at scale.

When evaluating a framework, performance matters. We've benchmarked qqq against common enterprise workloads and want to share the results along with optimization techniques we've learned from production deployments.
Performance is a feature, not an afterthought. We've optimized qqq's runtime to handle enterprise workloads, and we're sharing the benchmarks and techniques that keep our largest deployments running smoothly.
Benchmark Methodology
Our benchmarks use realistic enterprise scenarios rather than synthetic tests. We measure:
- API Response Time: End-to-end latency for CRUD operations
- Throughput: Requests per second under sustained load
- Query Performance: Complex queries with joins, filters, and aggregations
- Memory Efficiency: Heap usage under various workloads
- Startup Time: Cold start to serving requests
All tests run on standardized infrastructure: 4 vCPU, 8GB RAM, PostgreSQL 15, with JDK 17.
API Response Time Benchmarks
| Operation | p50 | p95 | p99 | |-----------|-----|-----|-----| | Single record GET | 3ms | 8ms | 15ms | | List (25 records) | 12ms | 25ms | 45ms | | List with filters | 15ms | 35ms | 60ms | | Single INSERT | 8ms | 18ms | 30ms | | Bulk INSERT (100) | 45ms | 85ms | 120ms | | UPDATE | 6ms | 15ms | 25ms | | DELETE | 5ms | 12ms | 20ms |
These numbers include JSON serialization, authentication validation, and audit logging. The metadata layer adds approximately 1-2ms overhead compared to hand-written endpoints.
Throughput Under Load
We tested sustained load using a realistic mix of operations (60% reads, 30% writes, 10% complex queries):
| Concurrent Users | Requests/sec | Avg Response | Error Rate | |------------------|--------------|--------------|------------| | 50 | 850 | 12ms | 0% | | 100 | 1,450 | 18ms | 0% | | 250 | 2,800 | 35ms | 0% | | 500 | 4,200 | 65ms | 0.1% | | 1000 | 5,100 | 120ms | 0.8% |
Throughput scales linearly until CPU saturation around 500 concurrent users on this hardware profile.
Query Performance
Complex queries are where frameworks often struggle. Here's how qqq handles common patterns:
// Query: Orders with line items, customer, and product details
// Filters: Last 30 days, status = SHIPPED, total > $100
// Sort: By order date descending
// Pagination: 25 records
QueryInput query = new QueryInput()
.withTableName("order")
.withFilter(new QQueryFilter()
.withCriteria("orderDate", GREATER_THAN, thirtyDaysAgo)
.withCriteria("status", EQUALS, "SHIPPED")
.withCriteria("total", GREATER_THAN, 100))
.withOrderBy(new QFilterOrderBy("orderDate", false))
.withIncludeAssociations(List.of("lineItems", "customer"))
.withLimit(25);
// Execution time: 18ms average (with proper indexes)Query Optimization Results
| Query Type | Unoptimized | Optimized | Improvement | |------------|-------------|-----------|-------------| | Simple filter | 8ms | 3ms | 2.7x | | Multi-table join | 85ms | 22ms | 3.9x | | Aggregation query | 120ms | 35ms | 3.4x | | Full-text search | 250ms | 45ms | 5.5x |
Optimization techniques include proper indexing, query hints, and leveraging qqq's built-in query planning.
Memory Efficiency
qqq's metadata-driven architecture has a fixed memory overhead regardless of table count:
| Tables Defined | Heap at Startup | Heap Under Load | |----------------|-----------------|-----------------| | 10 | 180MB | 350MB | | 50 | 195MB | 380MB | | 100 | 220MB | 420MB | | 500 | 310MB | 550MB |
Adding more tables increases metadata storage but doesn't multiply memory usage like traditional ORMs that generate entity classes.
Startup Performance
Cold start times matter for serverless deployments and rapid scaling:
| Configuration | Startup Time | |---------------|--------------| | Minimal (5 tables) | 1.2s | | Standard (25 tables) | 2.1s | | Large (100 tables) | 4.5s | | Enterprise (500 tables) | 12s |
Most of startup time is JVM initialization and dependency injection. The metadata loading phase is under 500ms for typical applications.
Optimization Best Practices
1. Index Strategy
qqq generates efficient SQL, but it can't create indexes for you. Add indexes for:
-- Filter fields
CREATE INDEX idx_order_status ON orders(status);
CREATE INDEX idx_order_date ON orders(order_date);
-- Foreign keys (if not auto-created)
CREATE INDEX idx_order_customer ON orders(customer_id);
-- Composite indexes for common query patterns
CREATE INDEX idx_order_status_date ON orders(status, order_date DESC);2. Query Filter Optimization
Order your filter criteria by selectivity (most selective first):
// Good: Status (few values) filters before date range
new QQueryFilter()
.withCriteria("status", EQUALS, "PENDING") // ~5% of records
.withCriteria("orderDate", GREATER_THAN, lastWeek) // ~15% of records
// Less optimal: Date first processes more records
new QQueryFilter()
.withCriteria("orderDate", GREATER_THAN, lastWeek)
.withCriteria("status", EQUALS, "PENDING")3. Association Loading
Use includeAssociations judiciously:
// Only load what you need
QueryInput query = new QueryInput()
.withTableName("order")
.withIncludeAssociations(List.of("customer")) // Just customer, not all associations
// Avoid N+1 with explicit association loading
// qqq batches association queries automatically4. Pagination
Always paginate large result sets:
QueryInput query = new QueryInput()
.withTableName("order")
.withLimit(25) // Page size
.withSkip(25 * page); // Offset for pagination
// For large datasets, use cursor-based pagination
QueryInput query = new QueryInput()
.withTableName("order")
.withFilter(new QQueryFilter()
.withCriteria("id", GREATER_THAN, lastSeenId))
.withLimit(25);5. Caching Strategy
qqq supports multiple caching layers:
// Table-level caching for reference data
new QTableMetaData()
.withName("country")
.withCachePolicy(new QCachePolicy()
.withTTL(Duration.ofHours(24))
.withMaxSize(500));
// Use the Cache qBit for complex caching needs
qInstance.addQBit(new CacheQBit()
.withProvider(new RedisProvider(redisUrl))
.withDefaultTTL(Duration.ofMinutes(15)));Scaling Strategies
Horizontal Scaling
qqq applications are stateless by default, making horizontal scaling straightforward:
# Kubernetes deployment
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 3
template:
spec:
containers:
- name: qqq-app
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"With a load balancer, you can scale to handle thousands of concurrent users.
Read Replicas
For read-heavy workloads, configure read replicas:
QBackendMetaData backend = new QBackendMetaData()
.withName("primary")
.withConnection(primaryConnection)
.withReadReplicas(List.of(
new DatabaseConnection().withJdbcUrl(replica1Url),
new DatabaseConnection().withJdbcUrl(replica2Url)
));
// qqq routes read queries to replicas automaticallyConnection Pooling
Tune your connection pool for your workload:
DatabaseConnection connection = new DatabaseConnection()
.withJdbcUrl(jdbcUrl)
.withPoolSize(20) // Connections per instance
.withMaxIdleTime(600) // Seconds before idle connection cleanup
.withConnectionTimeout(30); // Seconds to wait for connectionRule of thumb: poolSize = (connections_per_request * concurrent_requests) + buffer
Production Monitoring
Monitor these metrics in production:
// qqq exposes metrics via Micrometer
// Configure your preferred backend (Prometheus, DataDog, etc.)
// Key metrics to watch:
// - qqq.api.requests (counter by endpoint, status)
// - qqq.api.latency (histogram)
// - qqq.db.queries (counter by table, operation)
// - qqq.db.latency (histogram)
// - qqq.cache.hits/misses (counter)Set alerts for:
- p99 latency > 500ms
- Error rate > 1%
- Connection pool exhaustion
- Memory usage > 80% of limit
Comparison to Hand-Written Code
The question we often hear: "How does qqq compare to hand-optimized code?"
| Aspect | Hand-Written | qqq | Delta | |--------|--------------|-----|-------| | Simple CRUD | Baseline | +5-10% overhead | Negligible | | Complex queries | Varies | Consistent | Often faster* | | Development time | 100% | 20-30% | 3-5x faster | | Bug rate | Higher | Lower | Fewer edge cases |
*qqq's query builder applies consistent optimizations that developers often miss in hand-written SQL.
The metadata overhead (1-2ms per request) is offset by consistent optimization patterns and significantly faster development time.
Conclusion
qqq delivers enterprise-grade performance out of the box. The metadata-driven approach adds minimal overhead while providing consistency, auditability, and rapid development that hand-written code can't match.
For most applications, qqq performs excellently without tuning. For high-scale deployments, the optimization techniques above can push performance further.
Questions about performance? Join our Discord or check out the performance tuning guide.