Every repeated aggregation in this experiment asked MongoDB to do the same work again. I wanted to see what would happen if the application could return repeat reads from a cache instead.
Momento asked me to evaluate its technology and write about it. I approached the test as an AWS Data Hero who usually reached for Amazon ElastiCache for Redis. I received no compensation, unless you count a tumbler as ample compensation. I did make great connections with the Momento team and joined its mission to #CacheTheWorld.
This post records my initial 2022 impressions of setting up Momento Cache with MongoDB Atlas. It preserves the original claims, code, and results, but several details no longer describe the current products:
- Amazon DocumentDB describes broad MongoDB compatibility alongside documented functional differences, rather than 100% compatibility.
- Momento now separates dedicated Cache Cluster and Flex from Cache Serverless. Cluster and Flex currently run on AWS and accept standard Valkey or Redis clients; their capacity-based pricing differs from Serverless usage pricing.
- The current Cache Serverless Python setup uses
MOMENTO_API_KEYandMOMENTO_ENDPOINT, not the historicalMOMENTO_AUTH_TOKENshown below. - MongoDB Atlas provider-to-provider failover depends on a configured multi-cloud topology and a voting majority; it is not an unconditional property of every Atlas deployment.
Treat the setup, APIs, credentials, service footprints, and pricing context below as historical, and check the current documentation before implementing the example today.
Why I tried a separate cache service
Out of the gate, I found Momento required far less setup ceremony than the caching services I was used to. I still wanted an API compatible with Redis or Memcached so that a business could move existing code with few changes beyond configuration.
Adding another vendor can create its own concern. But slow responses carry a cost too: one study reported that a two-second wait could drive website visitors elsewhere. After application and database tuning, caching can be another way to avoid repeating expensive work on the request path.
At the time, I was also drawn to Momento’s positioning as a cache specialist with a multi-cloud footprint. That seemed useful for applications that remained on premises, wanted to reduce dependence on one cloud, or were pursuing a multi-cloud strategy.
The architecture I had in mind
For a small website with few operational demands, I would be comfortable running everything on AWS. For a client worried about single-provider risk and demanding very high availability with low latency, I would consider distributing the system across vendors while retaining a fail-fail-fail-over path—still not a typo—to one cloud provider.
My hypothetical website used AWS Amplify, Amazon DocumentDB, AWS Lambda, and Amazon API Gateway. A request would pass through API Gateway to a Lambda function. The function would query DocumentDB and return a JSON response through API Gateway to the Amplify application.
I wanted to explore a less cloud-specific data path. In my 2022 framing, active-active infrastructure was costly, and moving database traffic between regions could require application and configuration changes that put a recovery time objective at risk.
Moving the example to MongoDB Atlas
I started with DocumentDB as the transactional store. I described its API as 100% compatible with then-current MongoDB versions and treated a move to MongoDB Atlas as a way to put the database with a specialized provider. Atlas could place a database across regions and cloud providers, then fail over between them.
For the experiment, I used a free Atlas account and its sample_analytics data. From a laptop on a 100 Mb/s internet connection, I queried the transactions collection for purchases of a particular stock. The aggregation unwound each account’s transactions, selected Adobe (adbe) buy transactions, and grouped the matching items by account:
['sample_analytics']['transactions'].aggregate([
{
'$unwind': {
'path': '$transactions'
}
}, {
'$match': {
'$and': [
{
'transactions.transaction_code': 'buy',
'transactions.symbol': 'adbe'
}
]
}
}, {
'$group': {
'_id': '$_id',
'transaction_item': {
'$push': '$transactions'
}
}
}
])
If an Atlas query does not return from a local program, check Security > Network Access in the Atlas UI. I have lost time more than once because my current IP address was not allowed to reach the database.
Adding a cache-aside path
Once the query worked in Atlas and in Python with pymongo, I added Momento using its Python SDK. The 2022 setup used the Momento CLI and an authentication token delivered during account setup. I first created a cache, wrote one key-value pair, and read it back.
The combined program used a cache-aside pattern: look in the cache first, fetch from the database after a miss, and put that result into the cache for later requests. MongoDB remained the source of truth. The cache held a copy that the application could recreate after it expired.
I modeled a service that lets auditors look up recent employee trades. The sample assumed that this data changed infrequently enough for the experiment’s cache lifetime. That assumption bounded the demo; it was not a production consistency policy.
The read sequence was:
- A user logs in and looks up a ticker symbol.
- The program derives a cache key from the ticker and transaction action, such as
adbe-buyormsft-sell. - The program asks Momento for that key.
- On a miss, it runs the MongoDB aggregation, stores the result in Momento, and returns the result.
- On a hit, it returns the cached result without running the aggregation.
What the 2022 timing showed
With SKIP_CACHE=True, the program ran the same aggregation 10 times in sequence. Those 10 retrievals consistently took about five seconds in total, or an average of 0.5 seconds each.
On the first cache-aside run, the program missed once, stored the result in Momento, and then hit the cache nine times. That run took a little more than three seconds. On the next run, all 10 reads came from Momento and completed in less than three seconds. Using three seconds as the comparison point gives an average of 0.3 seconds per read—a 40% reduction from the observed uncached average.
These were end-to-end wall-clock observations from one laptop, not an isolated service benchmark. The experiment did not measure concurrent traffic, tail latency, cost, or production load. It also did not separate time spent in the client, network, serialization, and services. The result only showed that this cache-aside path avoided enough repeated aggregation work to improve this particular run.
Questions to answer before production
The demo’s happy path is deliberately small. A production design still needs explicit answers to several questions:
- Freshness: How stale may a result become, and what time to live matches that limit?
- Invalidation: Which database writes should delete or refresh the corresponding key?
- Key scope: Does the key represent every query parameter and authorization boundary that can change the result?
- Failure behavior: If the cache is unavailable or returns an error, should the application query MongoDB, fail the request, or degrade another way?
- Miss bursts: How will the application prevent many simultaneous misses from repeating the same expensive database work?
- Security: Where will credentials live, and which networks and identities may reach each service?
The sample program prints a cache error rather than falling back to MongoDB, so it does not answer the failure question. Its assumption that the trade data changes infrequently also does not replace an invalidation strategy. Those omissions are acceptable for reproducing the timing, but not for operating the pattern as written.
Python 3.11 notes from the experiment
I began the post with Python 3.9 and moved to Python 3.11 during the final push. I appreciated that the newer version made the missing authentication token clear when I forgot it:
$ python async-main.py
...
File "/Users/rob.koch/Library/Python/3.11/lib/python/site-packages/momento/_momento_endpoint_resolver.py", line 26, in resolve
return _getEndpointFromToken(auth_token)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/rob.koch/Library/Python/3.11/lib/python/site-packages/momento/_momento_endpoint_resolver.py", line 37, in _getEndpointFromToken
raise errors.InvalidArgumentError("Invalid Auth token.") from None
momento.errors.InvalidArgumentError: Invalid Auth token.
The 2022 command passed the token in an environment variable:
DEBUG=true \
MOMENTO_AUTH_TOKEN=* \
python3 async-main.py
Treat that command as a record of the old setup, not current credential guidance. Use a secret-management approach appropriate to your environment and follow the current SDK documentation.
What I took away
The interesting result was not that every MongoDB query should be cached. It was that one small cache-aside path stopped nine repeated aggregations on its first run and all 10 on the next. For a read that is expensive to rebuild and safe to serve within a defined freshness window, that can free the database to focus on work only it can do.
The tradeoff is that the application now owns freshness, invalidation, key design, and failure behavior. If those boundaries fit your workload, clone the sample program and repeat the experiment with one of your own aggregations. Before connecting it to Momento, follow the current Momento Cache Serverless Python SDK guide rather than the historical setup above.