User Guide
Everything you need to install, configure, and build with Montycat — from getting started to governance, advanced operations, and security.
Advanced
Change number of default permits
Montycat automatically determines the number of simultaneous operations it can handle based on system resources such as available memory, CPU cores, and disk capacity.
By default, the system allocates 500 permits per thread.
You can override this value if needed.
Note: increasing the number of permits does not guarantee better performance.
Setting this value too high may exhaust system resources and degrade overall stability.
For example, on a system with 8 threads, configuring 1000 permits per thread results in a total of 8000 permits.
The change requires a restart.
After restart, Montycat automatically redistributes permits across all background services and tasks.
montycat default-permits 8000or
montycat console
> default-permits 8000
exitSet host and port
By default, Montycat binds to 0.0.0.0:21210 (and 21211 for subscription connections), which means it listens on all available network interfaces at ports 21210/21211. You can override this behavior by specifying a dedicated IP address and/or a custom port.
Note: Port changing will require a restart.
montycat setup --host 192.168.170.17 --port 21218
# subscription server will be set to 21219 by defaultor
montycat console
> setup host 192.168.170.17 port 21218
exitAllow reporting / Disable reporting
By default, Montycat asynchronously with no performance impact collects anonymous usage statistics to help improve the product.
This data includes information about performance metrics, bugs and issues.
No personal or sensitive data is collected.
You can disable this feature if you prefer not to share usage data.
montycat disable-reportsor
montycat console
> disable-reports
exitYou can also enable reporting.
montycat enable-reportsor
montycat console
> enable-reports
exitYou can also set reporting flag using Docker
FROM montygovernance/montycat:latest
# MONTYCAT_REPORTS_ALLOWED default is true so it can be overridden with MONTYCAT_REPORTS_ALLOWED=false
ENV MONTYCAT_REPORTS_ALLOWED=falseEnable wait-for-index
For maximum speed and productivity, any CRUD operation does not wait for indexing Write-Ahead Log (WAL) to complete in persistent keyspaces. You can enable or disable the "wait-for-index" option based on your requirements. Then all your operations will wait for indexing to complete.
Enabling the indexing wait can guarantee durability for write-heavy workloads, but may slightly slow down write operations.
In case of In-Memory keyspaces wait-for-index feature will be automatically enabled as you enable snapshots.
montycat console
> enable-wait-for-index
exitto disable it
montycat console
> disable-wait-for-index
exitYou can also toggle wait-for-index directly from the client:
# Make CRUD operations wait for the index to catch up (read-after-write) on
# persistent keyspaces. Guarantees durability for write-heavy workloads at the
# cost of slightly slower writes.
await connection.enable_wait_for_index()
# {"status": True, "payload": None, "error": None}
# Back to fire-and-forget indexing (the default, faster writes):
await connection.disable_wait_for_index()Beyond the DB-wide switch, every write accepts a per-request wait_for_index argument that overrides the default for that single call — useful when one write needs read-after-write guarantees but the rest can stay fast. It is a no-op on in-memory keyspaces.
from keyspaces import Employees #keyspaces.py
# Per-request override on a single persistent write: force THIS insert to wait
# for indexing, regardless of the DB-wide default. (No-op on in-memory keyspaces.)
await Employees.insert_value(record, wait_for_index=True)
# Also available on update_value, insert_bulk, update_bulk, etc.
await Employees.update_value(key, {"location": "Location"}, wait_for_index=True)Queue depths
Montycat processes indexing, timestamps, sharding, and counting off the write hot path through internal task queues. You can inspect how much work is pending in each queue — useful for monitoring backpressure and tuning throughput on write-heavy workloads.
# Inspect background task queue depths — how much work is pending across the
# engine's internal queues. Useful for monitoring backpressure and throughput.
depths = await connection.queue_depths()
# {"status": True, "payload": { ...per-queue depths... }, "error": None}Connection pooling
By default every request opens a connection, sends, reads one response, and closes it. Reuse the connection instead and the handshake disappears from every call after the first. The win scales with how much of your latency is connection setup: large for a chatty service issuing many small reads, larger over a network — where the handshake costs a full round trip before the query is even sent — and larger again with TLS. The Rust client measures 2.56x faster on loopback against a debug engine, which is the conservative end of the range.
Pooling is opt-in: one new argument on the engine, and no call site changes.
# connection.py
from montycat import Engine, PoolConfig, close_all_pools
connection = Engine(
host="<host>",
port=<port>,
username="<username>",
password="<password>",
store="<store name>",
pool=PoolConfig(), # the only new argument; omit for connect-per-request
)
# Tune it if you need to. Defaults: max_idle=8, idle_timeout=30.0 seconds.
# pool=PoolConfig(max_idle=4, idle_timeout=15.0)
# Nothing at the call sites changes.
res = await Employees.insert_value({"username": "Name", "age": 21})
# Before the process exits, and from a connectivity change if you observe one.
await close_all_pools()In the Python, Node, and Dart clients pools live in a module-level registry keyed by host, port, and TLS, so every keyspace pointing at one server shares a single pool rather than each opening its own. TLS is part of the key because a plaintext and an encrypted connection to the same address are not interchangeable.
The Rust client is the exception: pooling is per-engine. Reuse one engine for the process lifetime — cloning is cheap and shares the pool, while building a fresh engine per request builds a fresh empty pool and amortises nothing.
An idle pooled connection still holds one of the engine's connection permits, so a large pool across many client processes can starve the server while mostly idle. The defaults are deliberately conservative — 8 idle connections, dropped after 30 seconds — and should be raised only after measuring with queue depths under realistic load.
Close the pool before the process exits. In Node idle sockets otherwise keep the process alive; in Rust, where Drop cannot be async, a TLS connection would otherwise close without close_notify and the server logs an error for each one.
Subscriptions are never pooled: they are long-lived, stream many responses to one request, and live on their own port.
iOS and Android close sockets when an app is backgrounded, so every pooled connection is dead on resume. The Dart client detects that before reusing one and opens a fresh connection, but the cost is a user-visible round trip — prefer a short idleTimeout on mobile.
Connectivity changes invalidate the pool as well: Wi-Fi to cellular kills every pooled connection. If your app already observes connectivity, call closeAllPools() on a change rather than discovering it one failed request at a time. Because of backgrounding, pooling helps least on mobile and most on server-side Dart.