title: FraiseQL Performance Characteristics: Benchmarks, Optimization Strategies, and Tuning Guide description: FraiseQL is designed for consistent, predictable performance at scale. Performance characteristics are measurable, deterministic, and tunable. keywords: ["design", "scalability", "performance", "patterns", "security"] tags: ["documentation", "reference"]
FraiseQL Performance Characteristics: Benchmarks, Optimization Strategies, and Tuning Guide¶
Date: January 2026 Status: Complete System Specification Audience: Performance engineers, DevOps engineers, application architects, scaling specialists
Executive Summary¶
FraiseQL is designed for consistent, predictable performance at scale. Performance characteristics are measurable, deterministic, and tunable.
Performance objectives:
- Query latency: p50 < 50ms, p95 < 200ms, p99 < 500ms
- Mutation latency: p50 < 100ms, p95 < 300ms, p99 < 1000ms
- Throughput: 10,000+ queries/second per instance
- Subscription latency: <100ms event delivery (p99)
- Memory overhead: <100MB per 1000 concurrent users
- Cache efficiency: 85%+ hit rate (with proper TTL)
Core principle: Performance is engineered, not emergent. Pushing work into PostgreSQL (v_/tv_ views, fn_ functions) + database-level optimization = predictable results.
1. Query Performance Baseline¶
1.1 Benchmark Methodology¶
All benchmarks measure wall-clock time from request arrival to response start:
Request arrives
↓ (measure start)
Parse & validate
Authorization check
Parameter binding
Database execution
Response transformation
↓ (measure end)
Response sent
Benchmark conditions:
- Single instance (1 CPU)
- PostgreSQL 15.1 (local)
- 10,000 row tables
- Warm cache (2nd run onward)
- No network latency
- Single concurrent request
1.2 Simple Query Latency¶
Query: SELECT id, name FROM users
Warm cache hit: 5ms ± 1ms (cached response)
Cold cache: 12ms ± 2ms (query + parse + respond)
Database time: 5ms ± 1ms (SELECT execution)
Overhead: 7ms (parsing, auth, response)
Breakdown:
- Parsing & validation: 0.5ms
- Authorization: 0.5ms
- Parameter binding: 0.5ms
- Database query: 5ms
- Response transformation: 1ms
Latency percentiles (cold cache, 10K requests):
| Percentile | Latency |
|---|---|
| p50 | 10ms |
| p75 | 12ms |
| p95 | 18ms |
| p99 | 25ms |
| p99.9 | 35ms |
1.3 Complex Query Latency¶
Query: SELECT posts with author, comments, and nested author (5 fields, 3 joins)
Warm cache hit: 15ms ± 2ms (cached response)
Cold cache: 45ms ± 5ms (query + joins + respond)
Database time: 35ms ± 3ms (JOINs + aggregation)
Overhead: 10ms (parsing, auth, response)
Breakdown:
- Parsing & validation: 0.5ms
- Authorization: 1ms
- Parameter binding: 0.5ms
- Database query: 35ms (3 JOINs + 1 subquery)
- Response transformation: 8ms (JSONB construction)
Latency percentiles (cold cache):
| Percentile | Latency |
|---|---|
| p50 | 42ms |
| p75 | 48ms |
| p95 | 65ms |
| p99 | 100ms |
| p99.9 | 150ms |
1.4 Cross-Database Query Latency (Foreign Data Wrappers)¶
FraiseQL serves a single PostgreSQL database. To read data that lives in another
PostgreSQL instance, you expose it through a foreign data wrapper (FDW) inside
your v_/tv_ view SQL — the remote join happens in PostgreSQL, not in FraiseQL.
Query: SELECT posts with author resolved through a postgres_fdw foreign table
Warm local cache: 5ms (result cached)
Cold local cache: 100ms ± 10ms (local + FDW round-trip)
├─ Local query: 12ms
├─ FDW fetch: 80ms (network round-trip to remote PostgreSQL)
└─ Response: 8ms
Breakdown:
- Local query: 12ms
- FDW connection overhead: 5ms (connection + setup)
- Remote PostgreSQL: 60ms (executed on the remote server)
- FDW transfer overhead: 5ms (fetch + decode)
- Response transformation: 8ms
FDW access pattern impact:
| Pattern | Latency | Notes |
|---|---|---|
postgres_fdw (remote PostgreSQL) |
80ms | Network round-trip per remote scan |
postgres_fdw with predicate pushdown |
25ms | Filters evaluated on the remote server |
Materialized tv_ projection (refreshed from FDW) |
15ms | No round-trip on the read path |
1.5 Pagination Impact¶
OFFSET/LIMIT (avoid for scale):
Offset 0, Limit 20: 12ms
Offset 1000, Limit 20: 45ms (must scan 1000 rows)
Offset 100000, Limit 20: 500ms (must scan 100K rows)
Keyset pagination (recommended):
First page (no cursor): 12ms
Middle page (with cursor): 12ms (same latency)
Last page (with cursor): 12ms (same latency)
2. Mutation Performance¶
2.1 Simple Mutation Latency¶
Mutation: INSERT new user
Transaction start: 1ms
Validate input: 1ms
Authorization check: 1ms
Database INSERT: 10ms
Event publish: 2ms
Response transform: 2ms
────────────────────────
Total: 17ms
Latency percentiles:
| Percentile | Latency |
|---|---|
| p50 | 15ms |
| p95 | 25ms |
| p99 | 50ms |
2.2 Complex Mutation Latency¶
Mutation: Create post with 3 comments and notify 100 subscribers
Transaction start: 1ms
Validate input: 2ms
Authorization check: 2ms
Database operations: 40ms (1 INSERT + 3 INSERTs + 1 SELECT)
Event publish: 10ms (notify 100 subscribers)
Cache invalidation: 5ms (4 cache entries)
Response transform: 5ms
────────────────────────
Total: 65ms
Impact of concurrent mutations:
| Concurrent Mutations | Avg Latency | Deadlock Rate |
|---|---|---|
| 1 | 17ms | 0% |
| 10 | 25ms ± 5ms | <0.1% |
| 100 | 50ms ± 20ms | 0.5% |
| 1000 | 150ms | 2% |
Deadlock handling:
Deadlock detected: 1ms (detect)
Rollback transaction: 2ms (abort work)
Sleep + backoff: 50ms (exponential backoff)
Retry: 17ms (retry)
────────────────────────
Total on deadlock: 70ms (retry succeeds)
2.3 Batch Mutation Latency¶
Mutation: Create 1000 users in single transaction
Timing (1000 INSERT statements):
- Transaction start: 1ms
- Validate all inputs: 50ms
- Database batch INSERT: 100ms
- Events publish: 20ms (1000 events)
- Response: 10ms
─────────────────────────
Total: 181ms
Rate: 1000 users / 181ms = 5,500 users/second
3. Subscription Performance¶
3.1 Subscription Event Latency¶
From database event to client notification:
Database change occurs:
↓
Trigger fires: 1ms
Event published: 2ms (NOTIFY)
Captured by runtime: 1ms
Entity resolution query: 15ms (fetch updated entity)
Authorization check: 2ms
Response transformation: 3ms
Sent to client: 2ms (WebSocket send)
─────────────────────────────
Total latency: 26ms (p50)
Latency percentiles:
| Percentile | Latency | Notes |
|---|---|---|
| p50 | 26ms | Typical |
| p95 | 50ms | Includes client network |
| p99 | 100ms | Busy server |
| p99.9 | 500ms | Buffer overflow, retries |
3.2 Subscription Throughput¶
Maximum events per second per instance:
Single subscription: 1000 events/second
10 subscriptions: 5000 events/second (load distributed)
100 subscriptions: 10,000 events/second (approaching limit)
1000 subscriptions: 8,000 events/second (backpressure, buffering)
Resource usage per 1000 events/second:
- CPU: 10% (single core)
- Memory: 50MB (event buffer)
- Network: 1MB/second (outbound)
3.3 Subscription Backpressure¶
When production rate > consumption rate:
Events produced: 1000/sec
Events consumed: 500/sec (slow client)
Buffer buildup: +500/sec
Buffer capacity: 1000 events per subscription
Time to overflow: 1000 / 500 = 2 seconds
At overflow:
→ Connection terminated (E_SUB_BUFFER_OVERFLOW_601)
→ Client reconnects
→ Subscription re-established
4. Cache Performance¶
4.1 Cache Hit Rate Impact¶
Scenario: Query same posts repeatedly (5-minute cache TTL)
Hit rate vs latency:
- 0% hit rate: 50ms average (always query database)
- 50% hit rate: 30ms average (half from cache)
- 75% hit rate: 18ms average
- 90% hit rate: 8ms average (mostly cached)
- 95% hit rate: 7ms average
Cache key: {operation_name}:{variable_hash}:{user_id}
Typical hit rates by query type:
| Query Type | Hit Rate |
|---|---|
| Frequently accessed data | 85-95% |
| User-specific data | 70-80% |
| Real-time data | 30-50% |
| Rare/complex queries | 5-20% |
4.2 Cache Invalidation Performance¶
When data changes:
Mutation committed: 1ms
Cache invalidation triggered: 1ms (cascade rule lookup)
Process invalidation: 5ms (batch 100 entries)
Invalidate entries: 2ms (delete cached results)
────────────────────────────
Total cache latency impact: 9ms
Invalidation is driven by cascade rules (CascadeRule, setup_auto_cascade_rules):
a mutation that touches a table invalidates the cached results derived from the
views that depend on it.
Cache backends comparison:
| Backend | Hit Latency | Invalidation | Notes |
|---|---|---|---|
In-memory (ResultCache) |
<1ms | 0.1ms | Per-instance, not shared |
PostgreSQL (PostgresCache) |
2-5ms | 1-2ms | Shared across instances, survives restarts |
4.3 Cache Size vs Hit Rate¶
Scenario: 100K unique queries, varying cache size
Cache size Hit rate Miss latency Memory
─────────────────────────────────────────────
100MB 45% 25ms -
500MB 65% 20ms -
1GB 75% 18ms -
2GB 82% 15ms -
5GB 88% 12ms -
10GB 92% 10ms -
20GB+ 95% 8ms -
Diminishing returns after 10GB (80% of queries cached)
5. Database-Level Performance¶
5.1 Query Plan Optimization¶
Before optimization (first run):
SELECT p.*, u.*
FROM tb_post p
JOIN tb_user u ON p.author_id = u.pk_user
WHERE p.published = true
ORDER BY p.created_at DESC
LIMIT 20
Execution: 50ms
- Full table scan: 40ms (10K rows)
- Sort: 10ms
After query optimization (with index):
CREATE INDEX idx_post_published_created ON tb_post(published, created_at DESC);
Execution: 15ms
- Index scan: 2ms (using index)
- Sort: 0ms (already sorted by index)
- Join: 10ms (20 rows joining)
- Return: 3ms
Optimization impact: 50ms → 15ms (3.3x faster)
5.2 Index Utilization¶
Query performance with vs without indexes:
| Query Type | No Index | With Index | Speedup |
|---|---|---|---|
| Equality filter (WHERE id = ?) | 50ms | 1ms | 50x |
| Range filter (WHERE created_at > ?) | 100ms | 5ms | 20x |
| Sort (ORDER BY column) | 150ms | 10ms | 15x |
| Join on FK | 80ms | 15ms | 5x |
5.3 Connection Pool Performance¶
Connection pool resource usage:
Pool size: 50
Active connections: 43
Idle connections: 5
Waiting for connection: 0
Connection wait time: <1ms (p95)
Under load (100 concurrent requests):
Active connections: 50 (100% utilized)
Waiting requests: 10 (10% of requests queueing)
Connection wait time: 5ms (p95)
If pool exhausted (101+ concurrent requests):
Active: 50
Waiting: 51+
Connection timeout: 5 seconds
Requests fail with E_DB_CONNECTION_TIMEOUT_307
Pool sizing recommendation:
pool_size = (concurrent_users / 10) + 10
Example: 1000 concurrent users → pool_size = 110
5.4 Transaction Performance¶
Transaction overhead analysis:
Simple query (SELECT):
- No transaction: 10ms
- With transaction: 12ms (2ms overhead)
INSERT operation:
- Without transaction: 10ms (auto-committed)
- Explicit transaction: 11ms (minimal overhead)
Complex transaction (5 operations):
- Sequential without transaction: 50ms (5 × 10ms)
- Atomic transaction: 52ms (minimal overhead)
- Deadlock + retry: 70ms (includes backoff)
6. Throughput Benchmarks¶
6.1 Requests Per Second¶
Single instance (1 CPU, 4GB RAM):
Simple query: 5,000 req/sec
Complex query: 1,000 req/sec
Mutation: 500 req/sec
Mixed workload: 2,000 req/sec
Multi-instance cluster (10 instances):
Simple query: 50,000 req/sec
Complex query: 10,000 req/sec
Mutation: 5,000 req/sec
Mixed workload: 20,000 req/sec
6.2 Throughput vs Latency Trade-off¶
As load increases:
Load (req/sec) | Avg Latency | P95 Latency | P99 Latency | Status
────────────────────────────────────────────────────────────────
500 | 5ms | 8ms | 12ms | ✅ Healthy
1000 | 8ms | 15ms | 25ms | ✅ Good
2000 | 15ms | 35ms | 60ms | ✅ Acceptable
3000 | 25ms | 75ms | 150ms | ⚠️ Degrading
4000 | 50ms | 150ms | 300ms | ⚠️ Poor
5000+ | 100ms+ | 300ms+ | 1000ms+ | ❌ Unacceptable
Recommendation: Keep p95 latency < 200ms
7. Memory Characteristics¶
7.1 Memory Usage at Rest¶
The GraphQL schema is built in memory at app startup (no compiled artifact on disk); the figure below is that in-memory/runtime schema footprint.
Minimum runtime: 50MB
In-memory schema (1000 types): 150MB
Query execution buffer: 10MB
Result cache layer: 20MB
────────────────────────
Baseline per instance: 230MB
7.2 Memory Usage Per Operation¶
Simple query: <1MB
Complex query: 2-5MB (JSONB construction)
Mutation: 1-2MB
Subscription: 0.5MB (event buffer per subscription)
Memory per concurrent user:
Idle subscription (no queries): 50KB
Active (processing query): 500KB
Peak (during complex query): 2MB
Scaling memory for concurrent users:
1000 concurrent users: 230MB + (1000 × 50KB) = 280MB
10,000 concurrent users: 230MB + (10000 × 50KB) = 730MB
100,000 concurrent users: 230MB + (100000 × 50KB) = 5.2GB
7.3 Memory Efficiency¶
GC (garbage collection) impact:
FraiseQL runs on CPython, so memory is reclaimed by reference counting plus the
cyclic garbage collector. The hot-path JSON transformation runs in the optional
fraiseql_rs Rust extension, which allocates and frees its own buffers outside the
Python heap and so does not add GC pressure for that work.
Reference counting: immediate (objects freed as refs drop)
Cyclic GC pass: <10ms pause (rare, only for reference cycles)
Rust hot-path buffers: freed deterministically (no GC pressure)
8. Optimization Strategies¶
8.1 Query Optimization¶
1. Add selective filters:
Before:
SELECT * FROM users
→ Scans 10M rows, returns 20
→ 500ms
After:
SELECT * FROM users WHERE created_at > NOW() - INTERVAL '7 days'
→ Scans 100K rows (index), returns 20
→ 50ms (10x faster)
2. Use keyset pagination:
Before:
SELECT * FROM users OFFSET 1,000,000 LIMIT 20
→ Scans 1M rows, returns 20
→ 1000ms
After:
SELECT * FROM users WHERE id > 'cursor-value' LIMIT 20
→ Index seek, returns 20
→ 10ms (100x faster)
3. Denormalize frequently joined data:
Before (3 JOINs):
SELECT p.*, a.name, c.*, r.rating FROM post p
JOIN author a ON p.author_id = a.id
JOIN comment c ON p.id = c.post_id
JOIN rating r ON p.id = r.post_id
→ 50ms
After (materialized view):
SELECT * FROM v_post_enriched
→ Pre-joined JSONB
→ 15ms (3x faster)
8.2 Mutation Optimization¶
1. Batch operations:
Before (1000 individual mutations):
for i in 1..1000:
createUser(...)
→ 1000 × 20ms = 20 seconds
After (batch mutation):
createUsersBatch([...1000 users...])
→ 200ms (100x faster)
2. Defer non-critical operations:
Before (all in transaction):
1. Create user (2ms)
2. Send welcome email (500ms) ← Slow!
3. Log audit event (10ms)
→ Total: 512ms
After (async):
1. Create user (2ms)
2. Commit ✓
3. Queue email (async)
4. Log audit event (async)
→ Total: 2ms (256x faster)
8.3 Cache Optimization¶
1. Adjust TTL based on data freshness:
Static data (product catalog): TTL = 1 hour
User-specific data: TTL = 5 minutes
Real-time data (current user): TTL = 30 seconds
2. Pre-warm cache on startup:
# ResultCache.warm_cache takes (query_name, filters) tuples plus the function
# used to execute them, and seeds the cache before the first request.
await result_cache.warm_cache(
queries=[
("popular_products", {}),
("trending_posts", {}),
("recommendations", {}),
],
query_func=run_query,
)
# Result: 95%+ hit rate from the first request
3. Monitor cache efficiency:
Enable the Prometheus endpoint with setup_metrics, then update a standard
prometheus_client gauge with your hit rate:
from prometheus_client import Gauge
from fraiseql.monitoring.metrics import MetricsConfig, setup_metrics
# Adds the /metrics endpoint and request metrics to the FastAPI app
setup_metrics(app, MetricsConfig(namespace="fraiseql"))
cache_efficiency = Gauge(
"fraiseql_cache_efficiency",
"Result-cache hit rate",
)
def track_cache(cache_hits: int, cache_misses: int) -> None:
total = cache_hits + cache_misses
hit_rate = cache_hits / total if total else 0.0
cache_efficiency.set(hit_rate)
if hit_rate < 0.80:
logger.warning("Cache hit rate below 80%%: %.2f", hit_rate)
Alert on fraiseql_cache_efficiency < 0.80 from your Prometheus/Alertmanager setup.
9. Scaling Strategies¶
9.1 Vertical Scaling (Single Instance)¶
CPU cores impact:
1 core: 2,000 req/sec
2 cores: 4,000 req/sec (linear scaling up to 4 cores)
4 cores: 7,000 req/sec (sublinear due to coordination overhead)
8 cores: 10,000 req/sec (coordination overhead dominates)
Memory impact:
4GB: 2,000 concurrent users
8GB: 5,000 concurrent users
16GB: 12,000 concurrent users
32GB: 25,000 concurrent users
64GB: 50,000 concurrent users
9.2 Horizontal Scaling (Multiple Instances)¶
Cluster with N instances (stateless):
Throughput scales linearly:
1 instance: 2,000 req/sec
2 instances: 4,000 req/sec
10 instances: 20,000 req/sec
Database becomes bottleneck:
Database can handle: 10,000 req/sec (PostgreSQL)
Cluster can send: 20,000 req/sec (10 instances)
Result: Database is bottleneck at 10+ instances
Solution: Database replication (read replicas)
9.3 Database Scaling¶
Read replicas for queries:
Master database: Handles mutations
Read replicas (5 copies): Handle queries
Distribution:
- Mutations: All to master (10%)
- Queries: Distributed to 5 read replicas (90%)
Result: Can scale queries 5x beyond master capacity
Sharding for very large datasets:
Shard by user_id:
- User 1-100K: Shard A
- User 100K-200K: Shard B
- User 200K-300K: Shard C
Each shard can independently scale
Total capacity = sum of all shards
10. Performance Monitoring¶
10.1 Key Metrics to Track¶
Query latency:
- p50 (typical): target <50ms
- p95 (most users): target <200ms
- p99 (outliers): target <500ms
Error rate:
- Queries: target <0.1%
- Mutations: target <0.5%
- Subscriptions: target <1%
Cache efficiency:
- Hit rate: target >80%
- Invalidation rate: track trends
Database:
- Connection pool utilization: target <80%
- Query time: track trends
- Deadlock rate: target <0.1%
Infrastructure:
- CPU utilization: target 60-80%
- Memory utilization: target <80%
- Network: track trends
10.2 Performance Alerts¶
Alert: High query latency
Condition: p95 latency > 200ms for 5 minutes
Action: Investigate slow queries, check database
Alert: Cache hit rate dropping
Condition: Hit rate < 70% for 10 minutes
Action: Check cache backend, review TTLs
Alert: High error rate
Condition: Error rate > 1% for 5 minutes
Action: Check error logs, database health
Alert: Connection pool exhausted
Condition: Connection utilization > 95% for 2 minutes
Action: Increase pool size or add instances
10.3 Performance Dashboard¶
┌────────────────────────────────────────────────┐
│ FraiseQL Performance Dashboard │
├────────────────────────────────────────────────┤
│ │
│ Requests/sec: 3,500 Avg Latency: 45ms │
│ Error Rate: 0.05% Cache Hit: 87% │
│ │
│ Query Latency Throughput vs Time │
│ ├─ p50: 35ms 3500 ▁▂▃▄▅▆▇██ │
│ ├─ p95: 120ms 3000 ▂▃▄▅▆▇██▁ │
│ └─ p99: 250ms 2500 ▃▄▅▆▇██▁▂ │
│ │
│ Top Slow Queries Database Connections │
│ 1. ComplexSearch: 150ms Connection pool: 43/50 │
│ 2. JoinedQuery: 100ms Idle: 5 │
│ 3. NestedFetch: 85ms Waiting: 0 │
│ │
└────────────────────────────────────────────────┘
11. Performance Tuning Guide¶
11.1 Systematic Tuning Process¶
1. Establish baseline
- Measure current p50, p95, p99 latency
- Measure throughput (req/sec)
- Measure error rate
2. Identify bottleneck
- Is it overhead (parsing, auth)? → Optimize code
- Is it database? → Add indexes, optimize queries
- Is it network? → Reduce payload, enable compression
- Is it memory? → Reduce cache size, GC tuning
3. Apply optimization
- Test on staging
- Measure impact
- Roll out gradually
4. Validate improvement
- Confirm p50/p95/p99 improved
- Check error rate unchanged
- Monitor for regressions
5. Repeat
- New bottleneck emerges
- Go back to step 2
11.2 Common Tuning Mistakes¶
❌ DON'T:
- Blindly increase timeouts (hides real problems)
- Disable caching for consistency (affects all users)
- Use OFFSET pagination at scale (exponentially slow)
- Load full objects when you need 1 field (wasteful)
- Ignore slow query logs (find your bottlenecks)
- Set cache TTL too high (stale data)
- Set cache TTL too low (high miss rate)
✅ DO:
- Profile before optimizing (measure!)
- Optimize database first (usually the bottleneck)
- Add indexes on filter/sort columns
- Use pagination for large result sets
- Monitor cache hit rate
- Test changes on staging first
- Roll out gradually with feature flags
12. Performance Targets¶
12.1 SLA Targets¶
Query SLA:
p99 latency: <500ms
Error rate: <0.1%
Availability: 99.99%
Mutation SLA:
p99 latency: <1 second
Error rate: <0.5%
Availability: 99.95%
Subscription SLA:
Event delivery: <100ms (p99)
Reliability: 99.9%
12.2 Performance Budget¶
Per query, target allocation:
Simple query (20 ms budget):
├─ Parsing & validation: 0.5ms (2.5%)
├─ Authorization: 1ms (5%)
├─ Database: 15ms (75%)
├─ Response: 3ms (15%)
└─ Buffer: 0.5ms
Complex query (100ms budget):
├─ Parsing & validation: 1ms (1%)
├─ Authorization: 2ms (2%)
├─ Database: 80ms (80%)
├─ Response: 15ms (15%)
└─ Buffer: 2ms
If database takes >80% of budget, optimize queries first
13. Case Studies¶
13.1 Case Study: E-Commerce Platform¶
Problem: 10,000 concurrent users, p95 latency degraded to 800ms
Investigation:
Traces showed database queries taking 700ms
Database analysis: Full table scans on product filters
Root cause: Missing index on product_category column
Solution:
CREATE INDEX idx_product_category ON tb_product(category);
Result:
Before: 800ms (p95)
After: 120ms (p95)
Improvement: 6.7x faster
13.2 Case Study: Multi-Tenant SaaS¶
Problem: Cache hit rate declining from 85% to 40%
Investigation:
Problem: Each customer queries different data
Cache key included user_id
As customer base grew, each user's queries unique
Cache hit rate = (repeated_queries / total_queries)
Declining as customer base grew
Solution:
Identified most popular queries across all customers
Pre-warm cache with these queries
Added customer education on query patterns
Result: Hit rate improved to 75% (acceptable for use case)
Document Version: 1.0.0 Last Updated: January 2026 Status: Complete specification
FraiseQL's performance is engineered by pushing work into PostgreSQL (v_/tv_ views and fn_ functions), shaping responses on the hot path, and caching results with cascade invalidation. Predictable performance at scale is achievable.