MemoryStore API
This page lists every endpoint on the MemoryStore service (memorystore-api), with request fields, response shapes, and the exact error messages the server returns. For a guided introduction, start with the MemoryStore overview and quickstart.
Base URL
MemoryStore has its own API service, separate from the core platform API, so it has its own base URL.
On the public API, https://api.codyhill.dev, which serves the MemoryStore routes alongside every other service. The platformctl memorystore commands read the same variable and fall back to that address, so you can leave it unset for them.
export CAI_MEMORYSTORE_API=https://api.codyhill.dev
Authentication
Every route except GET /healthz and GET /livez requires a bearer credential:
Authorization: Bearer <token-or-api-key>
The credential is either a session token (from POST /v1/auth/login on the core API, 12-hour life) or an API key (prefix cai_). Roles are re-read from the database on every request. See API authentication.
An unauthenticated call gets 401 with this exact message: authentication required: sign in via workload-api (POST /v1/auth/login) and send 'Authorization: Bearer <token>'.
| Role | Can call |
|---|---|
| member | list, get, stats, create |
| admin | everything a member can, plus delete and rotate-credential |
Conventions
- Error envelope. Every non-2xx response is
{"error": "<message>", "request_id": "<id>"}. - The 404 rule. "Not found" and "not yours" return byte-identical 404s. 403 appears only when you are a member but the action needs admin.
- Pagination.
page_size(1–200, default 50) andpage_token, withnext_page_tokenin responses, same as every platform list. - Request timeout. 30 seconds per request by default (
REQUEST_TIMEOUT_SECONDS). - Passwords are structural secrets. The instance object cannot contain a password — it has no field for one. The password appears in exactly two responses: create and rotate-credential.
Endpoints at a glance
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /v1/projects/{projectID}/memorystores | member | List instances |
| GET | /v1/projects/{projectID}/memorystores/{name} | member | Get one instance |
| GET | /v1/projects/{projectID}/memorystores/{name}/stats | member | Live server statistics |
| POST | /v1/projects/{projectID}/memorystores | member | Create an instance |
| DELETE | /v1/projects/{projectID}/memorystores/{name} | admin | Delete an instance |
| POST | /v1/projects/{projectID}/memorystores/{name}/rotate-credential | admin | Mint a new password |
| GET | /healthz | none | Readiness check |
| GET | /livez | none | Liveness check |
The instance object
Every read returns this shape:
{
"name": "sessions",
"resource_path": "projects/<short>/memorystores/sessions",
"project_id": "<uuid>",
"size_class": "medium",
"version": "9.1.0-alpine3.23",
"maxmemory_policy": "noeviction",
"persistence": {"enabled": true, "size": "2Gi"},
"state": "ready",
"ready": true,
"external_exposure": false,
"externally_reachable": false,
"external_endpoint": "rediss://sessions-<short>.<domain>:443",
"reachable_scope": "internet",
"memory_limit": "1Gi",
"maxmemory": "768Mi",
"ready_replicas": 1,
"replicas": 1,
"connection": {
"host": "<private-hostname>",
"port": 6379,
"username": "default",
"credential_secret": "ms-sessions-credential"
},
"conditions": [{"type": "Ready", "status": "True", "reason": "...", "message": "..."}],
"created_at": "2026-08-06T12:00:00Z"
}
| Field | Type | Meaning |
|---|---|---|
name | string | The instance name you chose. Immutable |
resource_path | string | Stable path: projects/<short>/memorystores/<name> |
project_id | string | The owning project's UUID |
size_class | string | small, medium, or large |
version | string | Server version; omitted when the platform default runs |
maxmemory_policy | string | Eviction policy (see create) |
persistence | object | enabled and volume size |
state | string | provisioning, ready, degraded, deleting, or unknown (nothing has reported on it yet). Lowercase, like every other resource's state |
ready | bool | The authoritative "you can connect now" signal, and the field to branch on |
message | string | Why it is not ready, when it is not. Omitted once it is |
external_exposure | bool | Whether TLS external access was requested on the instance itself. A cache reached through a published endpoint instead reports false here and true for externally_reachable — that is not a contradiction, it says which of the two paths put it on the internet |
externally_reachable | bool | Whether the cache is actually reachable from outside, by either path |
external_endpoint | string | rediss:// address; present only once there is one — the instance opted in and its route is programmed, or an endpoint was published for it |
reachable_scope | string | internet. It reported vpc until private-by-default landed; that was wrong, and a client that trusted it treated an internet-facing address as VPC-only |
memory_limit | string | Container memory limit |
maxmemory | string | The server's dataset cap (75% of the container limit) |
ready_replicas, replicas | int | Always a single server in the |
connection.host, connection.port | string, int | The instance's private address, reachable from your workloads on the platform (redis://, plaintext, port 6379) |
connection.username | string | Always default |
connection.credential_secret | string | Name of the stored credential that holds the password (ms-<name>-credential) — the name only, never the value |
conditions | array | CredentialReady, ServiceReady, WorkloadReady, Ready, plus ExternallyReachable for exposed instances |
created_at | string | RFC 3339 timestamp |
List instances
GET /v1/projects/{projectID}/memorystores
Auth: member.
| Name | Type | Required | Default | Notes |
|---|---|---|---|---|
page_size | int | no | 50 | 1–200 |
page_token | string | no | — | Opaque cursor |
Response 200:
{"memorystores": [], "next_page_token": "..."}
Get an instance
GET /v1/projects/{projectID}/memorystores/{name}
Auth: member. Returns 200 with the instance object; 404 otherwise.
Get live statistics
GET /v1/projects/{projectID}/memorystores/{name}/stats
Auth: member. Reads a live INFO snapshot directly from the running server. The internal read budget is 3 seconds.
Response 200:
{
"memorystore": "projects/<short>/memorystores/sessions",
"name": "sessions",
"used_memory_bytes": 1048576, "used_memory_human": "1.00M",
"maxmemory_bytes": 805306368, "maxmemory_policy": "noeviction",
"used_memory_pct": 0.13,
"connected_clients": 1, "blocked_clients": 0,
"keyspace_hits": 10, "keyspace_misses": 2, "hit_rate": 0.8333,
"evicted_keys": 0, "expired_keys": 0,
"total_commands_processed": 42, "instantaneous_ops_per_sec": 0,
"uptime_in_seconds": 3600, "db_keys": 12,
"note": "These are live values read directly from the running instance right now - a point-in-time snapshot. ..."
}
Field behavior worth knowing:
- Fields absent from the server's
INFOoutput are omitted, never zeroed. hit_rateis a 0–1 ratio and appears only after at least one lookup has happened.used_memory_pctappears only whenmaxmemoryis greater than 0.- This is a point-in-time snapshot. There is no time-series history.
Errors:
| Status | Message |
|---|---|
| 409 | this memorystore is not provisioned yet; wait until it reports ready, then read its stats |
| 503 | could not reach this memorystore to read its stats; it may be scaled down, still provisioning, restarting, or mid-credential-rotation - try again once it reports ready: ... |
| 502 | could not read the memorystore stats: ... |
Create an instance
POST /v1/projects/{projectID}/memorystores
Auth: member. Returns 201. Body limit: 16 KiB.
Request:
{
"name": "sessions",
"size_class": "medium",
"version": "",
"maxmemory_policy": "allkeys-lru",
"external_exposure": false,
"persistence": {"enabled": true, "size": "2Gi", "storage_class_name": ""}
}
| Field | Type | Required | Default | Rules |
|---|---|---|---|---|
name | string | yes | — | 1–40 chars, ^[a-z]([-a-z0-9]*[a-z0-9])?$ (lowercase letters, digits, hyphens, starting with a letter). Becomes the DNS name; immutable |
size_class | string | no | small | small, medium, or large |
version | string | no | platform default | Server version; blank means the platform default |
maxmemory_policy | string | no | noeviction | One of noeviction, allkeys-lru, allkeys-lfu, allkeys-random, volatile-lru, volatile-lfu, volatile-random, volatile-ttl |
external_exposure | bool | no | false | Opt-in TLS rediss:// endpoint, reachable from the internet; the password is the only boundary |
persistence.enabled | bool | no | true | To turn persistence off you must send {"persistence":{"enabled":false}} explicitly — omitting the field means on |
persistence.size | string | no | 2Gi | Volume size; fixed after create |
persistence.storage_class_name | string | no | — | Storage class for the data volume. Leave it blank unless your administrator told you which one to use |
Size classes (fixed; maxmemory is 75% of the container limit, by design, for OOM protection):
| Class | Container memory | Usable (maxmemory) |
|---|---|---|
small | 256Mi | 192Mi |
medium | 1Gi | 768Mi |
large | 4Gi | 3Gi |
Response 201 — the only response that contains the password, besides rotate:
{
"memorystore": {"name": "sessions", "state": "provisioning", "ready": false},
"credential": {
"username": "default",
"password": "<24-random-bytes base64url>",
"uri": "redis://:<password>@<private-hostname>:6379",
"external_uri": "rediss://:<password>@sessions-<short>.<domain>:443"
},
"note": "This is the only response that contains the password. Later reads return the name of the Secret holding it, never the value. The instance is still provisioning; poll until it reports ready before connecting."
}
The password is shown once here and once per rotation. Every later read returns only the name of the stored credential that holds it (ms-<name>-credential), never the value.
external_uri appears only when external exposure is on and the endpoint is already published — rare at create time; it typically appears on a later rotate.
Errors:
| Status | Message |
|---|---|
| 400 | invalid JSON body: ... |
| 400 | name must be 1-40 characters, lower-case letters, digits and hyphens, starting with a letter |
| 400 | sizeClass must be one of small, medium, large |
| 400 | maxmemoryPolicy must be one of ... |
| 409 | a memorystore named <name> already exists in this project |
| 502 | could not create the memorystore: ... |
Delete an instance
DELETE /v1/projects/{projectID}/memorystores/{name}
Auth: admin. Returns 202:
{"name": "sessions", "resource_path": "projects/<short>/memorystores/sessions",
"state": "deleting", "ready": false,
"note": "The instance and its data volume are being reclaimed. This is not reversible."}
Errors: 404 as usual; 502 could not delete the memorystore: ....
Rotate the credential
POST /v1/projects/{projectID}/memorystores/{name}/rotate-credential
Auth: admin. Empty body. Returns 200 with the same credential shape as create, and this note:
Rotation is not instantaneous. The server reads its password once, at start, so the new one takes effect when the instance restarts - which the platform triggers now. Until that rollout completes, the previous password is still the one in force.
Errors:
| Status | Message |
|---|---|
| 409 | this memorystore has no credential yet; wait for it to finish provisioning |
| 502 | could not rotate the credential: ... |
Health checks
GET /healthz
No auth. Readiness. Returns 200 or 503:
{"status": "ok", "checks": {
"orchestration": {"status": "ok"},
"memorystore_crd": {"status": "ok"},
"database": {"status": "ok"},
"session_key": {"status": "ok"}}}
Failing checks report unreachable, missing, or unconfigured, plus a verbatim detail field.
GET /livez
No auth. Liveness. Always returns 200 {"status":"ok"} while the process is up.
Server limits
| Limit | Value |
|---|---|
| Create body | 16 KiB |
| Request timeout | 30 s (REQUEST_TIMEOUT_SECONDS) |
| Stats read budget | 3 s |
| Instance name | 1–40 chars, starts with a letter |
List page_size | default 50, max 200 |
| Password appearances | create and rotate responses only |
See platform limits for cross-service limits.
Status codes
| Code | When |
|---|---|
| 400 | Invalid JSON; bad name, size class, or policy; malformed project id (projectID must be a UUID) |
| 401 | No or invalid credential |
| 403 | Member calling an admin route |
| 404 | Not found, or not yours — identical on purpose |
| 409 | Name taken; not provisioned yet (rotate, stats) |
| 429 | Per-principal rate limit |
| 502 | The platform's own provisioning system failed while handling your request |
| 503 | Health degraded; session signing key not loaded (this service cannot verify credentials: the shared session key is not loaded); stats unreachable |
Related pages
- MemoryStore overview — what the service is and when to use it
- Connect from workloads — using the stored credential instead of copying passwords around
- CLI: data and messaging commands — the read-only
platformctl memorystorecommands - API overview — shared conventions across all platform APIs