A Python library that provides client-side load balancing for ScyllaDB Alternator, wrapping boto3/aioboto3 to transparently distribute requests across cluster nodes.
- Automatic Load Balancing: Distributes requests across all available Alternator nodes using round-robin selection
- Node Discovery: Automatically discovers cluster topology via the
/localnodesendpoint - Topology Awareness: Route requests to specific datacenters or racks
- Key Affinity Routing: Optimizes LWT (Lightweight Transaction) operations by routing requests for the same partition key to the same node
- Request Compression: Optional gzip compression to reduce bandwidth
- Header Optimization: Filters unnecessary headers to reduce request overhead
- TLS Support: Full TLS/SSL support with custom CA certificates
- Async Support: Full async/await support via aioboto3
# Basic installation (sync client only)
pip install alternator-client
# With async support
pip install alternator-client[async]Note: The PyPI package name is
alternator-client, but the Python import remainsalternator.
from alternator import AlternatorConfig, AlternatorClient
# Configure the client
config = AlternatorConfig(
seed_hosts=["192.168.1.1", "192.168.1.2"],
port=8000,
)
# Use as a context manager (recommended)
with AlternatorClient(config) as client:
# Use like a normal boto3 DynamoDB client
response = client.list_tables()
print(response["TableNames"])
# Put an item
client.put_item(
TableName="my_table",
Item={
"pk": {"S": "user123"},
"data": {"S": "Hello, World!"},
}
)import asyncio
from alternator import AlternatorConfig
from alternator.async_client import AsyncAlternatorClient
async def main():
config = AlternatorConfig(
seed_hosts=["192.168.1.1"],
port=8000,
)
async with AsyncAlternatorClient(config) as client:
# Use like a normal aioboto3 DynamoDB client
response = await client.list_tables()
print(response["TableNames"])
asyncio.run(main())from alternator import AlternatorConfig
config = AlternatorConfig(
seed_hosts=["node1.example.com", "node2.example.com"],
port=8000,
scheme="http", # or "https" for TLS
)from alternator import (
AlternatorConfigBuilder,
CompressionAlgorithm,
KeyRouteAffinityMode,
TlsConfig,
)
config = (
AlternatorConfigBuilder()
.with_seeds("node1.example.com", "node2.example.com")
.with_port(8000)
.with_https(TlsConfig.system_default())
.with_datacenter("us-east-1")
.with_compression(CompressionAlgorithm.GZIP, min_size=1024)
.with_key_affinity(KeyRouteAffinityMode.RMW)
.with_refresh_intervals(active_ms=1000, idle_ms=60000)
.build()
)| Option | Type | Default | Description |
|---|---|---|---|
seed_hosts |
Sequence[str] |
(required) | Initial nodes for cluster discovery |
port |
int |
(required) | Alternator port |
scheme |
str |
"http" |
Protocol scheme ("http" or "https") |
routing_scope |
RoutingScope |
ClusterScope() |
Topology-aware routing |
compression |
CompressionAlgorithm |
NONE |
Request compression |
min_compression_size_bytes |
int |
1024 |
Minimum body size to compress |
optimize_headers |
bool |
False |
Enable header filtering |
headers_whitelist |
frozenset[str] |
None |
Additional headers to keep |
authentication_enabled |
bool |
True |
Include auth headers |
tls |
TlsConfig |
system default | TLS configuration |
key_affinity |
KeyRouteAffinityConfig |
NONE |
Key-based routing |
max_pool_connections |
int |
200 |
Max connections per host |
active_refresh_interval_ms |
int |
1000 |
Node refresh interval when active |
idle_refresh_interval_ms |
int |
60000 |
Node refresh interval when idle |
Control which nodes receive your requests based on topology:
from alternator import ClusterScope, DatacenterScope, RackScope
# Route to any node in the cluster (default)
config = AlternatorConfig(
seed_hosts=["node1"],
port=8000,
routing_scope=ClusterScope(),
)
# Route to nodes in a specific datacenter
config = AlternatorConfig(
seed_hosts=["node1"],
port=8000,
routing_scope=DatacenterScope(datacenter="us-east-1"),
)
# Route to nodes in a specific rack (with fallback)
config = AlternatorConfig(
seed_hosts=["node1"],
port=8000,
routing_scope=RackScope(datacenter="us-east-1", rack="rack1"),
)Scopes automatically fall back to broader scopes if no nodes are available:
RackScope→DatacenterScope→ClusterScope
For Lightweight Transactions (conditional writes), routing requests for the same partition key to the same node can improve performance:
from alternator import (
AlternatorConfigBuilder,
KeyRouteAffinityMode,
)
config = (
AlternatorConfigBuilder()
.with_seeds("node1")
.with_port(8000)
.with_key_affinity(
mode=KeyRouteAffinityMode.RMW, # Only for read-modify-write ops
table_pk_map={"my_table": "pk"}, # Optional: preload PK names
)
.build()
)| Mode | Description |
|---|---|
NONE |
Disabled (default round-robin) |
RMW |
Only for operations with ConditionExpression or ReturnValues |
ANY_WRITE |
For all write operations (PutItem, UpdateItem, DeleteItem) |
from alternator import TlsConfig, TlsSessionCacheConfig
from pathlib import Path
# Use system CA certificates (default)
tls = TlsConfig.system_default()
# Use custom CA certificate
tls = TlsConfig.with_custom_ca(Path("/path/to/ca.pem"))
# Trust all certificates (INSECURE - dev only)
tls = TlsConfig.trust_all()
# Full configuration
tls = TlsConfig(
custom_ca_cert_paths=[Path("/path/to/ca.pem")],
trust_system_ca_certs=True,
verify_hostname=True,
session_cache=TlsSessionCacheConfig(
enabled=True,
cache_size=1024,
timeout_seconds=86400,
),
)Enable gzip compression for large request bodies:
Note: Gzip request compression requires ScyllaDB 2026.1.0 or later. Earlier versions do not support the
Content-Encoding: gzipheader. Response compression (Accept-Encoding: gzip) is not yet supported by Alternator.
from alternator import AlternatorConfigBuilder, CompressionAlgorithm
config = (
AlternatorConfigBuilder()
.with_seeds("node1")
.with_port(8000)
.with_compression(
CompressionAlgorithm.GZIP,
min_size=1024, # Only compress bodies >= 1KB
)
.build()
)from alternator import (
AlternatorClient,
AlternatorConfig,
AlternatorError,
NoNodesAvailableError,
ConfigurationError,
)
try:
config = AlternatorConfig(seed_hosts=[], port=8000)
except ConfigurationError as e:
print(f"Invalid configuration: {e}")
try:
with AlternatorClient(config) as client:
client.list_tables()
except NoNodesAvailableError as e:
print(f"No nodes available: {e}")
except AlternatorError as e:
print(f"Alternator error: {e}")The library uses Python's standard logging module with the logger name alternator:
import logging
# Enable debug logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("alternator").setLevel(logging.DEBUG)Log levels:
INFO: Node discovery eventsWARNING: Fallback events, connection issuesDEBUG: Detailed routing decisions, node listsERROR: Failed operations
For table-oriented operations, use AlternatorResource which wraps boto3's DynamoDB resource:
from alternator import AlternatorConfig, AlternatorResource
config = AlternatorConfig(seed_hosts=["192.168.1.1"], port=8000)
with AlternatorResource(config) as resource:
table = resource.Table("my_table")
table.put_item(Item={"pk": "user123", "data": "hello"})
response = table.get_item(Key={"pk": "user123"})You can also use the factory function:
from alternator import create_resource, close_resource, AlternatorConfig
config = AlternatorConfig(seed_hosts=["node1"], port=8000)
resource = create_resource(config)
try:
table = resource.Table("my_table")
table.scan()
finally:
close_resource(resource)If you prefer not to use context managers:
from alternator import create_client, close_client, AlternatorConfig
config = AlternatorConfig(seed_hosts=["node1"], port=8000)
client = create_client(config)
try:
client.list_tables()
finally:
close_client(client) # Stop background refresh threadAsync equivalent:
from alternator import AlternatorConfig
from alternator.async_client import create_async_client, close_async_client
config = AlternatorConfig(seed_hosts=["node1"], port=8000)
client = await create_async_client(config)
try:
await client.list_tables()
finally:
await close_async_client(client)- Connection pool sizing: The default
max_pool_connections=200works for most workloads. Increase if you see connection pool exhaustion warnings under high concurrency. - Refresh intervals: Default active refresh (1s) is appropriate for dynamic clusters. For stable clusters, increase
active_refresh_interval_msto reduce discovery overhead. - Timeouts: Default
discovery_timeout_seconds=5.0andread_timeout_seconds=30.0are conservative. Tune based on your network latency and query complexity. - Monitoring: Enable
INFO-level logging for thealternatorlogger to track node discovery events. UseDEBUGfor detailed routing decisions during troubleshooting. - Seed hosts: Configure at least 2-3 seed hosts for redundancy in case one seed is temporarily unavailable during startup.
Sync clients created by create_client / AlternatorClient are thread-safe: the underlying node selection, round-robin counter, and node list updates are all protected by locks. You can safely share a single client across multiple threads.
Async clients created by create_async_client / AsyncAlternatorClient are safe to use from multiple concurrent coroutines within the same event loop. Do not share an async client across different event loops.
- Request Compression: Gzip compression requires ScyllaDB 2026.1.0+. Response compression is not yet supported by Alternator.
- TLS Session Cache Settings: The
cache_sizeandtimeout_secondsparameters inTlsSessionCacheConfigare not currently used by Python'ssslmodule. Only theenabledflag controls session ticket behavior. - Async Key Affinity: For async clients, partition key auto-discovery happens asynchronously. The first request for an unknown table will use round-robin routing while discovery runs in the background. Subsequent requests will use affinity. Preloading via
table_pk_mapavoids this initial miss. - Batch Operations: Key affinity routing does not support
BatchWriteItemoperations with items targeting different partition keys.
# Clone the repository
git clone https://github.com/scylladb/alternator-client-python.git
cd alternator-client-python
# Install in development mode
make install
# Run tests
make test-unit
# Run linting
make lint
# Start local Scylla cluster for integration tests
make scylla-start
make test-integration
make scylla-stopApache License 2.0
Contributions are welcome! Please read the contributing guidelines before submitting a pull request.