Home Playground Docs hub

Guide G-70 · Infrastructure & Storage

Caching with Memcache

Verified against local · 2026-09-25 · memcache-service 0.1.0 (workspace HEAD)
Journey. As a developer, I want a fast, shared, tenant-isolated cache
that any of my services can use — set a value with a TTL, read it back,
let it expire, and introspect what is cached — without running my own
Redis cluster.

The Memcache service is the platform's shared key-value cache: one in-memory store per tenant, AES-encrypted at rest, with exact-match lookups and per-entry TTLs. Platform services use it to memoize expensive computations; your services can use it the same way.

RouterPort (local)AudienceRoutes
Public:40011Your services & agentsadd, get, delete (+ MCP tools)
Private (loopback):40001Ops & platform services onlyprefix listing/deletion, per-tenant flush, diagnostics

Both routers require the tenant key pair and a user JWT (the Swagger UI and OpenAPI JSON are exempt). The private router exists on loopback precisely so administrative operations — listing every entry under a key prefix, flushing a tenant, reading hit-ratio counters — are never exposed to callers outside the host.

Prerequisites

POST /v3/auth/qa/login. Sandbox accepts any tenant id; the snippets use playground_a43b973c77e94b7f8b0e64d96c86edf8.

Concepts

weather:sf are unrelated entries. Semantic (similarity-based) caching is not implemented in this version of the service; plan your key names accordingly.

visible on the private router (and vice versa) — they share the same tenant cache. The split is about who may call which operation, not about separate data.

store; the private router's prefix listing decrypts them for you.

expires after roughly a second; a TTL of 0 is silently accepted but stores nothing (see Step 3).

Content-Type: text/plain; misses return an empty 404 (no JSON error body). add and delete likewise return empty bodies (201 / 200).

overflow the least-recently-used entry is evicted silently — a get you expected to hit can come back 404 under memory pressure.

Step 1 — Add a value and read it back

Obtain a user JWT, then add an entry and read it back on the public router:

bash
USER_TOKEN=$(curl -sf -X POST "${AUTH_URL}/v3/auth/qa/login" \
  -H "Content-Type: application/json" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -d "{\"device_id\": \"g70-demo\", \"locale\": \"en-US\", \"tenant_id\": \"${TENANT_ID}\", \"username\": \"g70-cacher\"}" \
  | python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])')
echo "JWT obtained (${#USER_TOKEN} chars)"

ADD_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
  "${MEMCACHE_URL}/v3/mem-cache/add/${TENANT_ID}" \
  -H "Content-Type: application/json" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}" \
  -d '{"key": "weather-cache-key", "value": "sunny 72F", "ttl_in_seconds": 300}')
echo "add status: ${ADD_CODE}"
test "${ADD_CODE}" = "201" && echo "entry stored"

Expected — add status: 201 with an empty body. The key must be 8–120 characters and the value at most 2 MiB. ttl_in_seconds is mandatory.

Reading back returns the raw value as text/plain — not a JSON envelope:

bash
GET_RESP=$(curl -s -w "\n%{http_code}" \
  "${MEMCACHE_URL}/v3/mem-cache/get/${TENANT_ID}/weather-cache-key" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}")
echo "${GET_RESP}"
VALUE=$(echo "${GET_RESP}" | sed '$d')
CODE=$(echo "${GET_RESP}" | tail -n 1)
test "${CODE}" = "200" && test "${VALUE}" = "sunny 72F" && echo "round trip verified"

Expected — the body is exactly sunny 72F followed by a captured 200 status line, then round trip verified. No JSON parsing needed — though the MCP get_cache_entry tool does wrap it in JSON (see Step 4).

Step 2 — Misses and deletes

A miss is a 404 with an empty body — distinguish it from an error by the status code alone (and from a stored empty string by calling add first). Delete returns 200 empty; afterwards the key misses:

bash
MISS_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
  "${MEMCACHE_URL}/v3/mem-cache/get/${TENANT_ID}/never-written-key" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}")
echo "miss status: ${MISS_CODE} (empty body)"
test "${MISS_CODE}" = "404" && echo "miss semantics verified"

DEL_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
  "${MEMCACHE_URL}/v3/mem-cache/delete/${TENANT_ID}/weather-cache-key" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}")
echo "delete status: ${DEL_CODE}"
test "${DEL_CODE}" = "200" || exit 1

GET_AFTER=$(curl -s -o /dev/null -w "%{http_code}" \
  "${MEMCACHE_URL}/v3/mem-cache/get/${TENANT_ID}/weather-cache-key" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}")
echo "get-after-delete status: ${GET_AFTER}"
test "${GET_AFTER}" = "404" && echo "delete verified"

Expected — miss status: 404, delete status: 200, then get-after-delete status: 404.

Step 3 — TTL expiry (and the ttl=0 gotcha)

A short TTL expires quickly; give a 1-second entry two seconds and it is gone. This is the mechanism behind "memoize for N seconds" patterns:

bash
curl -s -o /dev/null -w "add: %{http_code}\n" -X POST \
  "${MEMCACHE_URL}/v3/mem-cache/add/${TENANT_ID}" \
  -H "Content-Type: application/json" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}" \
  -d '{"key": "short-lived-key", "value": "ephemeral", "ttl_in_seconds": 1}'
sleep 2
CODE=$(curl -s -o /dev/null -w "%{http_code}" \
  "${MEMCACHE_URL}/v3/mem-cache/get/${TENANT_ID}/short-lived-key" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}")
echo "after 2s: ${CODE}"
test "${CODE}" = "404" && echo "TTL expiry verified"

Expected — add: 201, then after 2s: 404 and TTL expiry verified.

One wart to know: ttl_in_seconds: 0 is accepted with 201 but stores nothing — a subsequent get misses. If you treat "zero" as "no expiry" (as some cache APIs do), you will silently lose data. Always pass a real TTL:

bash
ZERO_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
  "${MEMCACHE_URL}/v3/mem-cache/add/${TENANT_ID}" \
  -H "Content-Type: application/json" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}" \
  -d '{"key": "zero-ttl-key", "value": "lost forever", "ttl_in_seconds": 0}')
echo "ttl=0 add status: ${ZERO_CODE} (looks successful...)"
GET_ZERO=$(curl -s -o /dev/null -w "%{http_code}" \
  "${MEMCACHE_URL}/v3/mem-cache/get/${TENANT_ID}/zero-ttl-key" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}")
echo "ttl=0 get status: ${GET_ZERO} (...but nothing was stored)"
test "${ZERO_CODE}" = "201" && test "${GET_ZERO}" = "404" && echo "ttl=0 no-op documented"

Expected — ttl=0 add status: 201, then ttl=0 get status: 404: a false-success gotcha. Use a generous TTL (or re-add on miss) instead.

Step 4 — The private router: list by prefix, flush, and diagnostics

The loopback router (:40001 locally) exposes administrative operations that are deliberately absent from the public surface. All of them need the same key pair and JWT — the router enforces authentication too.

Re-create two entries, then list everything under a key prefix (values come back decrypted):

bash
for kv in "g70-alpha:one" "g70-beta:two"; do
  KEY=${kv%%:*}; VAL=${kv#*:}
  curl -s -o /dev/null -w "add ${KEY}: %{http_code}\n" -X POST \
    "${MEMCACHE_URL}/v3/mem-cache/add/${TENANT_ID}" \
    -H "Content-Type: application/json" \
    -H "x-api-key: ${INAYA_API_KEY}" \
    -H "x-client-app-key: ${INAYA_APP_KEY}" \
    -H "Authorization: Bearer ${USER_TOKEN}" \
    -d "{\"key\": \"${KEY}\", \"value\": \"${VAL}\", \"ttl_in_seconds\": 600}"
done

LIST=$(curl -s \
  "${MEMCACHE_INTERNAL_URL}/v3/mem-cache-internal/${TENANT_ID}/entries/prefix/g70" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}")
echo "${LIST}" | python3 -c '
import json, sys
entries = json.load(sys.stdin)
keys = sorted(e["key"] for e in entries)
assert keys == ["g70-alpha", "g70-beta"], keys
values = {e["key"]: e["value"] for e in entries}
assert values["g70-alpha"] == "one" and values["g70-beta"] == "two", values
print("prefix list verified:", keys)
'

Expected — a 200 JSON array [{"key":"g70-alpha","shard":null,"value":"one"},…] (the shard field is currently always null) and prefix list verified. The prefix match is literal string-prefix on keys.

Diagnostics report entry counts and hit/miss counters — useful for cache effectiveness checks. The counters are process-global (across all tenants) and reset to zero when the service restarts, so assert deltas, not absolute values. Here we take a baseline, perform exactly one hit (the g70-alpha entry from above) and one miss, and verify the counters moved accordingly:

bash
COUNT=$(curl -s "${MEMCACHE_INTERNAL_URL}/v3/mem-cache-internal/diagnostics/entry-count" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}")
echo "entry count: ${COUNT}"
echo "${COUNT}" | python3 -c '
import json, sys
n = json.load(sys.stdin)["number_of_entries"]
assert n >= 2, n  # at least the two g70-* entries from above
print("entry count verified:", n)
'

BASE=$(curl -s "${MEMCACHE_INTERNAL_URL}/v3/mem-cache-internal/diagnostics/performance-stats" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}")
echo "baseline: ${BASE}"

curl -s -o /dev/null "${MEMCACHE_URL}/v3/mem-cache/get/${TENANT_ID}/g70-alpha" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}"
curl -s -o /dev/null "${MEMCACHE_URL}/v3/mem-cache/get/${TENANT_ID}/g70-never-written" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}"

AFTER=$(curl -s "${MEMCACHE_INTERNAL_URL}/v3/mem-cache-internal/diagnostics/performance-stats" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}")
echo "after 1 hit + 1 miss: ${AFTER}"

BASE_HITS=$(echo "${BASE}" | python3 -c 'import json,sys; print(json.load(sys.stdin)["cache_hits"])')
BASE_MISSES=$(echo "${BASE}" | python3 -c 'import json,sys; print(json.load(sys.stdin)["cache_misses"])')
BASE_ACCESS=$(echo "${BASE}" | python3 -c 'import json,sys; print(json.load(sys.stdin)["cache_accesses"])')
echo "${AFTER}" | BASE_HITS="${BASE_HITS}" BASE_MISSES="${BASE_MISSES}" BASE_ACCESS="${BASE_ACCESS}" python3 -c '
import json, os, sys
stats = json.load(sys.stdin)
assert stats["cache_hits"] == int(os.environ["BASE_HITS"]) + 1, stats
assert stats["cache_misses"] == int(os.environ["BASE_MISSES"]) + 1, stats
assert stats["cache_accesses"] == int(os.environ["BASE_ACCESS"]) + 2, stats
assert abs(stats["cache_hit_ratio"] - 100.0 * stats["cache_hits"] / stats["cache_accesses"]) < 0.01, stats
print("performance stats verified: +1 hit, +1 miss, ratio is a 0-100 percentage")
'

Expected — entry count verified: N with N ≥ 2 (the two g70-* entries, plus anything still live from Steps 1–3), then after 1 hit + 1 miss shows each counter up by exactly one (two for cache_accesses) versus the baseline, and cache_hit_ratio recomputed as a 0–100 percentage (e.g. 50.0 after an even mix). Only public-router get calls count toward these counters; private-router listing and diagnostics reads do not.

Finally, wipe the prefix and confirm it is gone. (Related routes: DELETE /v3/mem-cache-internal/admin/{tenant_id}/entries flushes one tenant, DELETE /v3/mem-cache-internal/admin/entries flushes all — the multi-tenant flush is for operators, not apps.)

bash
WIPE=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
  "${MEMCACHE_INTERNAL_URL}/v3/mem-cache-internal/${TENANT_ID}/entries/prefix/g70" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}")
echo "prefix wipe status: ${WIPE}"
test "${WIPE}" = "200" || exit 1

REMAIN=$(curl -s \
  "${MEMCACHE_INTERNAL_URL}/v3/mem-cache-internal/${TENANT_ID}/entries/prefix/g70" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}")
echo "remaining under prefix: ${REMAIN}"
test "${REMAIN}" = "[]" && echo "prefix wipe verified"

Expected — prefix wipe status: 200, then remaining under prefix: [].

Step 5 — Error contract and quirks

A few behaviors worth designing around — verified against the live service:

bash
# (a) Unauthenticated: 401 JSON, on both routers
UNAUTH=$(curl -s -w "\n%{http_code}" \
  "${MEMCACHE_URL}/v3/mem-cache/get/${TENANT_ID}/weather-cache-key" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}")
echo "${UNAUTH}"
echo "${UNAUTH}" | sed '$d' | grep -q '"Missing Authorization token"' \
  && test "$(echo "${UNAUTH}" | tail -n 1)" = "401" \
  && echo "unauthenticated 401 verified"

# (b) Short key: 400 plain text on add — but 500 on get (asymmetric wart)
SHORT_ADD=$(curl -s -w "\n%{http_code}" -X POST \
  "${MEMCACHE_URL}/v3/mem-cache/add/${TENANT_ID}" \
  -H "Content-Type: application/json" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}" \
  -d '{"key": "tiny", "value": "v", "ttl_in_seconds": 60}')
echo "${SHORT_ADD}"
SHORT_GET=$(curl -s -o /dev/null -w "%{http_code}" \
  "${MEMCACHE_URL}/v3/mem-cache/get/${TENANT_ID}/tiny" \
  -H "x-api-key: ${INAYA_API_KEY}" \
  -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}")
echo "short-key get status: ${SHORT_GET} (500, not 400 — known asymmetry)"
test "$(echo "${SHORT_ADD}" | tail -n 1)" = "400" && test "${SHORT_GET}" = "500" \
  && echo "short-key asymmetry documented"

Expected — (a) a 401 body {"error":"Missing Authorization token"}; (b) add rejects the 4-character key with 400 Key is too small (plain text), while the same short key on get returns 500 — a known asymmetry: validate key length client-side and treat any 5xx from get as "bad request shape", not "server down".

Two more quirks, stated without live demos:

accepts these fields and the private listing always reports "shard": null; do not rely on them for grouping or routing.

per-tenant capacity is 10 M entries with silent LRU eviction.

And the one thing this version does not do: semantic caching. There is no fuzzy/similarity lookup — get is exact-match only. (A semantic-match library ships in the workspace but is not wired into this service version; brochure claims to that effect are aspirational.)

Putting it together

A compact cache-aside pattern: check the cache, compute on miss, store with a TTL, and use the private router for introspection:

bash
# cache-aside read for "expensive-computation"
KEY="expensive-computation"
HIT=$(curl -s -o /tmp/g70-hit.txt -w "%{http_code}" \
  "${MEMCACHE_URL}/v3/mem-cache/get/${TENANT_ID}/${KEY}" \
  -H "x-api-key: ${INAYA_API_KEY}" -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}")
if [ "${HIT}" = "200" ]; then
  RESULT=$(cat /tmp/g70-hit.txt); echo "cache hit: ${RESULT}"
else
  RESULT=$(python3 -c 'import hashlib; print(hashlib.sha256(b"work").hexdigest()[:16])')
  echo "computed: ${RESULT}; storing for 5 min"
  curl -s -o /dev/null -X POST "${MEMCACHE_URL}/v3/mem-cache/add/${TENANT_ID}" \
    -H "Content-Type: application/json" \
    -H "x-api-key: ${INAYA_API_KEY}" -H "x-client-app-key: ${INAYA_APP_KEY}" \
    -H "Authorization: Bearer ${USER_TOKEN}" \
    -d "{\"key\": \"${KEY}\", \"value\": \"${RESULT}\", \"ttl_in_seconds\": 300}"
fi

# ops introspection on the loopback router
curl -s "${MEMCACHE_INTERNAL_URL}/v3/mem-cache-internal/diagnostics/performance-stats" \
  -H "x-api-key: ${INAYA_API_KEY}" -H "x-client-app-key: ${INAYA_APP_KEY}" \
  -H "Authorization: Bearer ${USER_TOKEN}"; echo

(MEMCACHE_URL / MEMCACHE_INTERNAL_URL are the public/loopback base URLs — http://127.0.0.1:40011 / http://127.0.0.1:40001 locally; AUTH_URL is http://127.0.0.1:40004; TENANT_ID is your tenant.)

Try it live

Open the Playground → Infrastructure & Storage — the Memcache console exercises add / get / delete against the live service.

Agents can also call the cache directly: the service exposes MCP tools add_cache_entry, get_cache_entry, and delete_cache_entry on the public router (/agent/v1/mcp, streamable HTTP, spec 2026-07-28), under the same key pair + JWT authentication.

Copy agent prompt

Agent prompt — copy and hand to your agent
You are working with the Inaya Labs Memcache service. The public router is
MEMCACHE_URL (:40011), the private loopback router is MEMCACHE_INTERNAL_URL
(:40001), Auth is AUTH_URL (:40004); the tenant key pair is in INAYA_API_KEY /
INAYA_APP_KEY and TENANT_ID. EVERY route on BOTH routers requires the key pair
headers AND a user JWT: obtain it via POST AUTH /v3/auth/qa/login
{"device_id","locale":"en-US","tenant_id","username"} -> .token. Steps:
(1) Add: POST MEMCACHE /v3/mem-cache/add/{tenant} {"key","value",
"ttl_in_seconds"} -> 201 EMPTY body. Key must be 8-120 chars; value <= 2 MiB;
ttl_in_seconds is REQUIRED and ttl=0 stores NOTHING (201 but immediate miss).
(2) Get: GET /v3/mem-cache/get/{tenant}/{key} -> 200 with the RAW value as
text/plain (not JSON); miss/expired -> 404 with EMPTY body. (3) Misses &
delete: DELETE /v3/mem-cache/delete/{tenant}/{key} -> 200 empty; a subsequent
get is 404. (4) TTL: ttl_in_seconds 1 expires after ~1-2 s (verify with sleep 2
then expect 404). (5) Private router (same auth!): GET
MEMCACHE_INTERNAL /v3/mem-cache-internal/{tenant}/entries/prefix/{prefix} ->
200 JSON array of {"key","shard":null,"value"} (decrypted); DELETE same path
wipes the prefix -> then list returns []; diagnostics GET
/v3/mem-cache-internal/diagnostics/entry-count -> {"number_of_entries":N} and
/v3/mem-cache-internal/diagnostics/performance-stats ->
{"cache_accesses","cache_hit_ratio","cache_hits","cache_misses"}. (6) Error
contract: no JWT -> 401 {"error":"Missing Authorization token"}; a short (<8
char) key -> 400 plain text on add but 500 on get (known asymmetry — validate
keys client-side); "tags"/"shard" fields are accepted on add but discarded;
lookups are EXACT MATCH ONLY — semantic caching is NOT implemented in this
version. Report status codes and bodies per step; never log the JWT.

API reference

Next journeys