| 1 | --- |
| 2 | title: Cache with Memorystore |
| 3 | description: Bind a Memorystore Redis instance to a Cloud Run service over Direct VPC egress, and what to expect from create and destroy times. |
| 4 | --- |
| 5 | |
| 6 | Memorystore is managed Redis on a private IP. That private IP is the |
| 7 | whole story: unlike Firestore or Pub/Sub, which a Cloud Run container |
| 8 | reaches over the public `googleapis.com` endpoint, Redis is only |
| 9 | reachable from inside your VPC. So a Memorystore binding needs two |
| 10 | things a normal binding does not — VPC egress on the service, and |
| 11 | patience while the instance is created. |
| 12 | |
| 13 | ## The service |
| 14 | |
| 15 | ```typescript |
| 16 | export default class Cached extends GCP.Function<Cached>()( |
| 17 | "Cached", |
| 18 | { |
| 19 | main: import.meta.url, |
| 20 | location: "us-central1", |
| 21 | invokerIamDisabled: true, |
| 22 | template: { |
| 23 | vpcAccess: { |
| 24 | egress: "PRIVATE_RANGES_ONLY", |
| 25 | networkInterfaces: [{ network: "default", subnetwork: "default" }], |
| 26 | }, |
| 27 | }, |
| 28 | }, |
| 29 | Effect.gen(function* () { |
| 30 | const cache = yield* GCP.Redis.Instance("Cache", { memorySizeGb: 1 }); |
| 31 | const redis = yield* GCP.Redis.ReadWriteRedis(cache); |
| 32 | |
| 33 | return { |
| 34 | fetch: Effect.gen(function* () { |
| 35 | yield* redis.set("probe", "ok").pipe(Effect.orDie); |
| 36 | const cached = yield* redis.get("probe").pipe(Effect.orDie); |
| 37 | return yield* HttpServerResponse.json({ redis: cached }); |
| 38 | }), |
| 39 | }; |
| 40 | }).pipe(Effect.provide(GCP.Redis.ReadWriteRedisHttp)), |
| 41 | ) {} |
| 42 | ``` |
| 43 | |
| 44 | `template.vpcAccess` with `PRIVATE_RANGES_ONLY` is Direct VPC egress: |
| 45 | the revision gets an interface on the network, and traffic to private |
| 46 | ranges goes through it while everything else still leaves normally. |
| 47 | Without it the container cannot open a socket to the instance and |
| 48 | every command times out. |
| 49 | |
| 50 | ## The client is `alchemy/Redis` |
| 51 | |
| 52 | `ReadWriteRedis` returns the same RESP client the Fly and Upstash |
| 53 | providers return, so `get`, `set`, `incr`, `expire`, and the rest |
| 54 | behave identically no matter who runs the server. Two narrower |
| 55 | bindings exist when you want least privilege at the call site: |
| 56 | |
| 57 | - `GCP.Redis.ReadRedis` / `ReadRedisHttp` — read commands only. |
| 58 | - `GCP.Redis.WriteRedis` / `WriteRedisHttp` — write commands only. |
| 59 | - `GCP.Redis.ReadWriteRedis` / `ReadWriteRedisHttp` — both. |
| 60 | |
| 61 | The binding needs no IAM role: RESP authenticates with the instance's |
| 62 | AUTH string. Alchemy resolves the connection URL (private IP, port, |
| 63 | AUTH string) when the instance reconciles and carries it to the runtime |
| 64 | through the host's runtime context, keyed per instance — so the private |
| 65 | IP is never something you paste in, and two instances bound to one host |
| 66 | never collide. |
| 67 | |
| 68 | ## Expect minutes, not seconds |
| 69 | |
| 70 | Creating a Memorystore instance takes several minutes; so does |
| 71 | deleting one. That is Google's provisioning time, not Alchemy's. Two |
| 72 | consequences worth knowing before you put this in a deploy loop: |
| 73 | |
| 74 | - Destroy waits out a `CREATING` instance and then retries `Conflict` |
| 75 | until the instance is actually gone, rather than failing on the |
| 76 | first race. |
| 77 | - The repository's live tests keep this path behind `GCP_TEST_REDIS=1` |
| 78 | so the normal suite is not held up by a ten-minute resource. |
| 79 | |
| 80 | For a cache that has to come up with the service, size the instance |
| 81 | once and leave it alone; treat it as slower-moving than the code |
| 82 | deployed in front of it. |
| 83 | |
| 84 | ## Next |
| 85 | |
| 86 | - [How bindings grant IAM](/gcp/guides/bindings). |
| 87 | - [Serve an API on Cloud Run](/gcp/guides/cloud-run-api). |
| 88 | - [Redis.Instance](/providers/gcp/redis/instance) reference. |