This document provides a comprehensive technical analysis of why the async-cassandra wrapper is necessary for modern async Python applications using Cassandra.
- Executive Summary
- 1. The Synchronous Paging Problem
- 2. Thread Pool Bottlenecks
- 3. Missing Async Context Manager Support
- 4. Lack of Async-First API Design
- 5. Event Loop Integration Issues
- 6. Callback-Based vs Async/Await Patterns
- 7. Resource Management in Async Contexts
- 8. Performance Implications
- Conclusion
While the cassandra-driver provides execute_async() for non-blocking query execution, it was designed before Python's async/await became standard. This creates several fundamental incompatibilities with modern async Python applications.
The fetch_next_page() method is synchronous and blocks the event loop:
# From cassandra-driver documentation
result = session.execute_async(query).result()
while result.has_more_pages:
result.fetch_next_page() # BLOCKS the event loop!- Official docs state: "latter pages will be transparently fetched synchronously" (DataStax Python Driver Paging)
- No
fetch_next_page_async()method exists in the API Reference - JIRA PYTHON-1261 tracks this limitation
- Blocks event loop for the duration of network round-trip to Cassandra
- Prevents processing other requests during this time
- Performance impact depends on latency and query patterns
The cassandra-driver uses a thread pool (Session.executor) for I/O operations:
# From cassandra/cluster.py
self.executor = ThreadPoolExecutor(max_workers=executor_threads)-
Thread Pool Exhaustion
- Default pool size from driver source:
min(2, get_cpu_count() // 2)(cluster.py line 1048) - Each blocking operation consumes a thread
- High concurrency quickly exhausts the pool
- Default pool size from driver source:
-
Context Switching Overhead
- Threads require OS-level context switches
- Much heavier than async task switches
- Increases latency and CPU usage
-
GIL Contention
- Python's Global Interpreter Lock creates contention
- Threads can't truly run in parallel for Python code
- Reduces effectiveness of threading
From the driver source (cassandra/cluster.py):
# Line 2087 in Session.__init__
self.executor = ThreadPoolExecutor(max_workers=executor_threads)
# Line 2718 in execute_async
future = self.executor.submit(self._execute, *args, **kwargs)
# This goes through thread pool, not event loopThe driver doesn't provide async context managers:
# This doesn't work with cassandra-driver
async with cluster.connect() as session: # Not supported
await session.execute(query)
# You can't do this either
async with cluster: # Not supported
pass- No automatic async cleanup
- Risk of resource leaks in async applications
- Inconsistent with Python async conventions
# Async context manager support (use carefully - see note below)
async with AsyncCluster(['localhost']) as cluster:
async with await cluster.connect() as session:
result = await session.execute(query)
# Automatic cleanup on exit
# IMPORTANT: For production applications, create cluster/session once:
cluster = AsyncCluster(['localhost'])
session = await cluster.connect()
# Reuse session for all requests - DO NOT close after each useNote: While async-cassandra provides context manager support for convenience in scripts and tests, production applications should create clusters and sessions once at startup and reuse them throughout the application lifetime. See the FastAPI example for the correct pattern.
The driver's API wasn't designed for async/await:
-
Futures vs Coroutines
# cassandra-driver returns ResponseFuture future = session.execute_async(query) result = future.result() # Not awaitable # async-cassandra returns coroutines result = await session.execute(query) # Natural async/await
-
No Async Iteration
# Can't do this with cassandra-driver async for row in result: await process(row)
-
Mixed Sync/Async APIs
- Some operations only sync (prepare, set_keyspace)
- Others only async through callbacks
- No consistent async interface
The driver doesn't integrate with asyncio's event loop:
# Driver uses its own event loop in libev
self._io_event_loop = EventLoop()
# This runs in a separate thread, not asyncio's loop- Two Event Loops: Driver's libev loop + asyncio loop
- Cross-Thread Communication: Overhead and complexity
- No Backpressure: Can't use asyncio's flow control
From driver architecture (connection.py):
- Uses libev/libuv for I/O loop (io/libevreactor.py)
- Runs in separate thread from asyncio
- Requires thread-safe communication
The driver uses callbacks, not async/await:
# cassandra-driver pattern
def callback(response):
# Handle response
pass
def err_callback(error):
# Handle error
pass
future = session.execute_async(query)
future.add_callback(callback)
future.add_errback(err_callback)- Callback Hell: Nested callbacks become unreadable
- Error Handling: Try/except doesn't work naturally
- No Stack Traces: Lost context in callbacks
from cassandra import InvalidRequest
# Clean async/await pattern
try:
result = await session.execute(query)
processed = await process(result)
await save(processed)
except InvalidRequest as e:
# Natural error handling with Cassandra exceptions
logger.error(f"Invalid query: {e}")-
Synchronous Shutdown
# cassandra-driver - shutdown methods are synchronous cluster.shutdown() # Synchronous method session.shutdown() # Synchronous method
-
No Graceful Async Cleanup
- Can't await pending operations
- No async connection draining
- Risk of resource leaks
# cassandra-driver - prepare() is synchronous
prepared = session.prepare(query) # Synchronous call
# async-cassandra provides async version
prepared = await session.prepare(query) # Non-blockingThe synchronous prepare() method is documented in the Session API
-
Thread Pool Overhead
- Each query requires thread allocation from limited pool
- Context switching between threads has OS overhead
- Python's GIL prevents true parallelism
-
Memory Usage
- Each thread requires its own stack space
- Coroutines share the same stack
- Memory difference becomes significant with many concurrent operations
-
Scheduling Overhead
- Thread scheduling is handled by the OS
- Coroutine scheduling is handled by Python's event loop
- Event loop scheduling is more efficient for I/O-bound operations
Performance improvements vary based on:
- Workload characteristics (query complexity, result size)
- Network latency to Cassandra cluster
- Concurrency level of the application
- Hardware resources available
The async approach shows the most benefit in high-concurrency scenarios where the thread pool becomes a bottleneck.
# Memory-efficient large result processing
async for row in await session.execute_stream(query):
await process(row) # Processes without loading all in memory- Built-in async metrics collection
- Integration with async monitoring systems
- No thread-safety concerns
- Async-aware retry logic
- Idempotency checking for safety
- Non-blocking retries
- Full type hints for async operations
- Better IDE support
- Catch errors at development time
Let's be completely honest: the cassandra-driver is fundamentally a synchronous, thread-based driver. No wrapper can change this underlying architecture. Here's what we're really dealing with:
From analyzing the source code:
-
All I/O is blocking (connection.py):
# Actual socket operations block the thread self._socket.send(data) # Blocks data = self._socket.recv(self.in_buffer_size) # Blocks
-
"Async" means thread pool (cluster.py):
def execute_async(self, query, ...): # Just submits to thread pool future = self._create_response_future(query, ...) self.submit(self._connection_holder.get_connection, ...) return future # Not an asyncio.Future!
-
ResponseFuture is not asyncio-compatible:
- Custom future implementation
- Callbacks run in thread pool threads
- No native event loop integration
The wrapper provides a bridge between the thread-based driver and asyncio:
# What happens under the hood
def _asyncio_future_from_response_future(response_future):
asyncio_future = asyncio.Future()
def callback(result):
# Bridge from thread to event loop
loop.call_soon_threadsafe(
asyncio_future.set_result, result
)
response_future.add_callback(callback)
return asyncio_future-
Developer Experience:
- Clean async/await syntax
- Natural error handling with try/except
- Integration with async frameworks (FastAPI, aiohttp)
- See our FastAPI example for a complete implementation
-
Event Loop Compatibility:
- Prevents blocking the event loop with synchronous calls
- Allows other coroutines to run while waiting for Cassandra
-
Async Paging (with caveats):
- Wraps page fetching to not block event loop
- But still uses threads under the hood
-
Thread Pool Overhead:
- Every query still goes through the thread pool
- Limited by thread pool size (default 2-4 threads) - see configuration guide
- Thread creation/switching overhead remains
-
True Async Performance:
- Not comparable to truly async drivers (like aioredis)
- Still limited by GIL and thread contention
- Cannot handle thousands of concurrent connections
-
Resource Usage:
- Threads still consume ~2MB each
- Thread pool must be sized appropriately
- Not as lightweight as pure coroutines
-
Fundamental Blocking Operations:
- Connection establishment blocks threads
- DNS resolution blocks threads
- SSL handshakes block threads
Instead of claiming "25x improvement", here's the reality:
- Best case: Prevents event loop blocking, allowing better concurrency
- Typical case: Similar throughput to sync driver, better latency distribution
- Worst case: Added overhead from thread-to-event-loop bridging
The real benefit is not blocking other operations, not magically making Cassandra faster.
Think of it this way: Without this wrapper, when your app queries Cassandra, it's like a cashier who stops serving all customers while waiting for a price check. With this wrapper, the cashier can help other customers while waiting. The price check doesn't get faster, but the line keeps moving!
Use async-cassandra when:
- You have an async application (FastAPI, aiohttp)
- You need to prevent blocking the event loop
- You want cleaner async/await syntax
- You're already committed to the cassandra-driver
Don't use it expecting:
- True async performance like aioredis or asyncpg
- To handle thousands of concurrent queries
- To eliminate thread overhead
- Magic performance improvements
| Aspect | True Async Driver (e.g., asyncpg) | async-cassandra |
|---|---|---|
| I/O Model | Non-blocking sockets | Blocking sockets in threads |
| Concurrency | Thousands of connections | Limited by thread pool |
| Memory | ~2KB per connection | ~2MB per thread* |
| CPU Usage | Single thread | Multiple threads + GIL |
| Performance | High | Moderate |
| Integration | Native async | Adapted via callbacks |
*Memory calculation: Each thread in Python requires approximately 1.5-2MB of memory. This is an empirical observation that can be verified:
# Measure actual thread memory overhead
import os
import psutil
import threading
import time
def worker():
# Keep thread alive
while True:
time.sleep(1)
# Measure baseline memory
process = psutil.Process(os.getpid())
baseline_mb = process.memory_info().rss / 1024 / 1024
# Create threads and measure memory increase
threads = []
for i in range(10):
t = threading.Thread(target=worker, daemon=True)
t.start()
threads.append(t)
time.sleep(0.1) # Let thread fully initialize
final_mb = process.memory_info().rss / 1024 / 1024
per_thread_mb = (final_mb - baseline_mb) / 10
print(f"Memory per thread: {per_thread_mb:.2f} MB")
# Typically shows 1.5-2.0 MB per threadThe memory includes:
- Thread stack space: Reserved virtual memory (not all used immediately)
- Thread-local storage: Python interpreter state per thread
- OS thread structures: Kernel data structures for thread management
- Python objects: Thread objects, locks, and synchronization primitives
A truly async Cassandra driver would require:
- Complete rewrite using non-blocking I/O
- Native asyncio protocol implementation
- Elimination of thread pools
- Direct event loop integration
This is a massive undertaking that the cassandra-driver team hasn't prioritized, likely because:
- Significant engineering effort required
- Need to maintain compatibility
- Current solution works for most use cases
The async-cassandra wrapper exists to solve a specific problem:
Modern async Python applications need to use Cassandra without blocking the event loop.
This wrapper provides:
- Event loop compatibility - Prevents blocking other operations
- Framework integration - Works with FastAPI, aiohttp, etc.
- Better developer experience - async/await instead of callbacks
- Streaming without blocking - Page fetching doesn't freeze your app
But let's be clear about what it is:
- A compatibility layer, not a performance enhancement
- A bridge between threads and asyncio, not true async I/O
- A practical solution, not a perfect one
If you need to use Cassandra from an async Python application, you have three choices:
- Use synchronous calls - Blocks your event loop, kills concurrency
- Use execute_async() directly - Callbacks, no async/await, still blocks on paging
- Use this wrapper - Clean async/await, no event loop blocking, but still threads underneath
Until someone writes a true async Cassandra driver from scratch (massive undertaking), this wrapper provides the best available solution for async Python applications that need Cassandra.
While the underlying architecture remains thread-based, this wrapper provides essential compatibility for async applications. When you need to integrate Cassandra with FastAPI or any async framework, having an interface that doesn't block your event loop is crucial for maintaining application responsiveness.
-
cassandra-driver source code
- GitHub Repository
- cluster.py - Session and thread pool implementation
- connection.py - Connection handling
- query.py - ResultSet implementation
-
DataStax Python Driver Documentation
- Official Documentation
- Query Paging - Paging documentation
- Asynchronous Queries
- API Reference - ResultSet
-
JIRA Issues (Note: Some JIRA links may require DataStax account access)
- PYTHON-1261 - Async iteration over result set pages
- PYTHON-1057 - Support async context managers
- PYTHON-893 - Better asyncio integration
- PYTHON-492 - Event loop integration issues
-
Python Documentation
-
Community Discussions
-
Performance References
This analysis is based on cassandra-driver version 3.29.2. The driver team may address some of these issues in future versions.