I wired long-term memory into an agent on Amazon Bedrock AgentCore, wrote a record, got a 201, and read back nothing. The record existed. The API said success. The read returned zero rows. It took me longer than I would like to admit to work out that all three of those were true at the same time.
AgentCore went GA on 13 October 2025, after a July preview. Memory is one of its newer pieces, and the docs are good on the happy path and quiet on the parts that bite. This is the operational truth of the write side, the bit you only learn by running it.
Two things bit me. One was an API that did not exist. The other was a write that succeeds and is not yet readable. Neither is in the tutorial.
The API that never existed
The codebase had a helper for persisting a fact to memory. It called client.ingest_memory_records(...). It read as correct. It had a docstring. It had a sensible name.
It had never run. It was written, never called, and so had never thrown. When I finally wired it into a real path, I checked the client first:
import boto3
c = boto3.client("bedrock-agentcore")
hasattr(c, "ingest_memory_records") # -> False
There is no ingest_memory_records operation on the AgentCore data plane. The helper would have raised AttributeError the first time anyone called it. Dead code that mirrors a real-sounding API is worse than no code, because it passes the eye test. A method name is not a fact. It is a claim, and an unrun claim is a guess wearing a fact's clothes.
The real operation is BatchCreateMemoryRecords. Confirm it against the SDK you actually ship, not against what the name suggests:
ops = c.meta.service_model.operation_names
[o for o in ops if "Memory" in o or "Record" in o]
# BatchCreateMemoryRecords, BatchDeleteMemoryRecords, BatchUpdateMemoryRecords,
# DeleteMemoryRecord, GetMemoryRecord, ListMemoryRecords, RetrieveMemoryRecords
Lesson one, before any of the memory detail: verify the operation exists in your pinned SDK version. The write API and the read API are not symmetric in name, and one of the plausible names is a trap.
Two tiers, and which one you are writing to
AgentCore Memory has two tiers, and the whole confusion comes from not knowing which one a given call touches.
Short-term memory is raw events. You write them with CreateEvent, one per turn or in batches, scoped to an actor and a session. This is conversation history. You do not semantically search it.
Long-term memory is extracted records, organised into namespaces. Normally these are produced asynchronously: a memory strategy runs in the background, reads your short-term events, and extracts or consolidates records into a namespace. The AWS docs are explicit that this generation is an async background process. You read long-term records with RetrieveMemoryRecords, a semantic search scoped to a namespace.
So if you want an agent to recall a durable fact on the next turn, it has to live in a long-term namespace that RetrieveMemoryRecords reads. Writing a CreateEvent and hoping the strategy extracts the right fields is slow and non-deterministic. There is a better path.
BatchCreateMemoryRecords writes directly into a long-term namespace. It is the bring-your-own-extraction door: you have already structured the fact, so you skip the strategy and put the record where the reader will look. The request is straightforward:
c.batch_create_memory_records(
memoryId=MEMORY_ID,
records=[{
"requestIdentifier": "seed-1",
"namespaces": ["/orgs/<tenant>/user/<user>/preferences/"],
"content": {"text": "Role: AE, mid-market SaaS. Prefers blunt feedback."},
"timestamp": now,
# memoryStrategyId is optional; see below
}],
)
Note there is no actorId argument. The actor is encoded into the namespace string. Get the namespace wrong and the write goes somewhere the reader never queries, which is its own quiet failure.
Success is not retrievability
Here is the part that cost me the afternoon. The write returns 201 with a successfulRecords entry and a memoryRecordId. Fetch that id directly and the record is there immediately:
r = c.batch_create_memory_records(memoryId=MEMORY_ID, records=[rec])
rid = r["successfulRecords"][0]["memoryRecordId"]
c.get_memory_record(memoryId=MEMORY_ID, memoryRecordId=rid) # exists, right away
Now query the namespace the way an agent actually would, and at five seconds it is empty:
c.retrieve_memory_records(
memoryId=MEMORY_ID,
namespace="/orgs/<tenant>/user/<user>/preferences/",
searchCriteria={"searchQuery": "role preferences", "topK": 10},
) # +5s -> 0 records
c.list_memory_records(memoryId=MEMORY_ID, namespace=NS) # +5s -> 0 records too
Both the semantic read and the plain namespace list returned nothing, while the record was fetchable by id the whole time. Poll for longer and the rows appear. So the record was created, addressable, and not yet indexed for namespace or semantic retrieval.
I ran that write-then-poll loop three times, fresh namespace each time, checking every three seconds. The semantic read first returned the record at 16, 27 and 15 seconds. The namespace list was similar, at 16, 23 and 15 seconds. Same account, same region, one sitting, so treat it as an order of magnitude, not a benchmark: budget fifteen to thirty seconds before a directly-written record is searchable, and measure your own.
The docs tell you long-term generation from events is asynchronous. They do not tell you that a direct BatchCreateMemoryRecords write also indexes asynchronously. I went looking, in the API reference and the memory guide, and could not find the read-after-write behaviour stated anywhere. You would reasonably assume the direct door skips the wait, because you did the extraction yourself. It does not skip the indexing.
The mental model that would have saved me the afternoon: 201 means accepted, not queryable. Read-after-write on a namespace is eventually consistent, on the order of tens of seconds. If you need certainty that a specific record landed, read it by id with GetMemoryRecord, which is immediate. If you need it to appear in a namespace search, wait for the index.
The one that was simpler than the docs implied
A smaller finding while I was in there. Long-term records carry an optional memoryStrategyId. I assumed retrieval might require the record's strategy to match the strategy that owns the namespace. It does not. I wrote two records to the same namespace, one with a memoryStrategyId and one without, and RetrieveMemoryRecords returned both. Retrieval matches on the namespace string alone. The strategy id is for associating a record with a strategy's consolidation, not a gate on reads. One less thing to get exactly right.
What I now do
- Treat
201fromBatchCreateMemoryRecordsas accepted, not queryable. The record is addressable by id immediately and searchable by namespace tens of seconds later. - If I need read-after-write certainty on a specific record, read it by id with
GetMemoryRecord, never by a namespace search. - Do not gate a "saved, now ask me" experience on instant recall. If a user saves a profile and immediately asks the agent what it knows about them, the honest answer for a few tens of seconds is nothing. For an onboarding flow this is fine, because there is natural delay before the first real turn. For a health check that writes then reads a namespace, it is a flake generator.
- Verify the operation exists in the pinned SDK before trusting a helper. A method name, a docstring, and a green diff are not evidence that a call is real.
None of this is a reason to avoid direct writes. They are the right call when you already have a structured fact, because the alternative is waiting on async extraction that may drop or reshape the fields you care about. It just means you design around two facts the docs bury: the name might not be a real operation, and the 201 means the service took your record, not that a reader can find it. Both looked like success. Neither was, until I actually read it back.
The hard part was never the memory model. It was the gap between what the API reports and what is true a second later.
Latency measured in a dev account, one region, three runs, polled at three-second granularity, so the real figure sits a little under each number. Retention and consolidation behaviour of directly-created long-term records I have not fully characterised yet. If your lag numbers differ, I would like to hear them.

Top comments (0)