diff --git a/examples/aws-redis-serverless/README.md b/examples/aws-redis-serverless/README.md
new file mode 100644
index 0000000000..5044582208
--- /dev/null
+++ b/examples/aws-redis-serverless/README.md
@@ -0,0 +1,107 @@
+# AWS Serverless Redis
+
+An example of deploying a Redis serverless cache using [Amazon ElastiCache Serverless](https://aws.amazon.com/elasticache/serverless/).
+
+This example demonstrates the new `serverless` option in SST's Redis component, which automatically scales based on usage and offers pay-per-use pricing.
+
+## Key Features
+
+- **Serverless Redis**: No instance management required
+- **Automatic Scaling**: Scales based on usage
+- **Pay-per-use**: Only pay for what you consume
+- **Simplified VPC**: No NAT Gateway required for serverless
+- **Same Client Interface**: Uses the same Redis client as traditional clusters
+
+## Get started
+
+1. **Clone and deploy**
+
+ ```bash
+ git clone https://github.com/sst/sst
+ cd sst/examples/aws-redis-serverless
+ npm install
+ sst deploy
+ ```
+
+2. **Test the Redis connection**
+
+ Once deployed, you can invoke the function to test the Redis connection:
+
+ ```bash
+ sst invoke MyApp
+ ```
+
+## Usage
+
+The serverless Redis is created with default limits:
+
+```ts title="sst.config.ts" {4-7}
+const redis = new sst.aws.Redis("MyRedis", {
+ vpc,
+ serverless: {
+ dataStorage: { maximum: 10, unit: "GB" },
+ ecpuPerSeconds: { maximum: 5000 }
+ }
+});
+```
+
+You can also enable serverless mode with defaults:
+
+```ts title="sst.config.ts" {3}
+const redis = new sst.aws.Redis("MyRedis", {
+ vpc,
+ serverless: true
+});
+```
+
+The client code remains exactly the same:
+
+```ts title="index.ts" {4-6,11-12}
+import { Cluster } from "ioredis";
+import { Resource } from "sst";
+
+const client = new Cluster([{
+ host: Resource.MyRedis.host,
+ port: Resource.MyRedis.port,
+}], {
+ redisOptions: {
+ tls: { checkServerIdentity: () => undefined },
+ username: Resource.MyRedis.username,
+ password: Resource.MyRedis.password
+ }
+});
+```
+
+## Architecture
+
+```
+┌──────────────┐ ┌─────────────────────┐
+│ Lambda │───▶│ ElastiCache │
+│ Function │ │ Serverless Redis │
+└──────────────┘ └─────────────────────┘
+ │ │
+ └──────────────────────┘
+ VPC Network
+```
+
+## Cost
+
+Serverless Redis pricing is based on:
+- **Data Storage**: Per GB stored
+- **ElastiCache Processing Units (ECPUs)**: Per second of processing
+
+Example cost for light usage:
+- Storage: 1 GB = ~$0.125/month
+- ECPUs: 1000 ECPU/second = ~$0.0034/hour
+
+This is significantly cheaper than traditional instances for variable workloads.
+
+## Differences from Traditional Redis
+
+| Traditional Redis | Serverless Redis |
+|-------------------|------------------|
+| Fixed instance costs | Pay-per-use pricing |
+| Manual scaling | Automatic scaling |
+| Requires NAT Gateway | Simplified networking |
+| Cluster/non-cluster modes | Simplified configuration |
+| Instance-based limits | Usage-based limits |
diff --git a/examples/aws-redis-serverless/index.ts b/examples/aws-redis-serverless/index.ts
new file mode 100644
index 0000000000..2109f870c8
--- /dev/null
+++ b/examples/aws-redis-serverless/index.ts
@@ -0,0 +1,30 @@
+import { Cluster } from "ioredis";
+import { Resource } from "sst";
+
+const client = new Cluster(
+ [
+ {
+ host: Resource.MyRedis.host,
+ port: Resource.MyRedis.port,
+ },
+ ],
+ {
+ redisOptions: {
+ tls: {
+ checkServerIdentity: () => undefined,
+ },
+ username: Resource.MyRedis.username,
+ password: Resource.MyRedis.password,
+ },
+ }
+);
+
+export async function handler() {
+ await client.set("foo", `bar-serverless-${Date.now()}`);
+ return {
+ statusCode: 200,
+ body: JSON.stringify({
+ foo: await client.get("foo"),
+ }),
+ };
+}
diff --git a/examples/aws-redis-serverless/package.json b/examples/aws-redis-serverless/package.json
new file mode 100644
index 0000000000..aa0cd6e8ad
--- /dev/null
+++ b/examples/aws-redis-serverless/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "aws-redis-serverless",
+ "version": "1.0.0",
+ "description": "",
+ "type": "module",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "ioredis": "^5.4.1",
+ "sst": "latest"
+ }
+}
diff --git a/examples/aws-redis-serverless/sst-env.d.ts b/examples/aws-redis-serverless/sst-env.d.ts
new file mode 100644
index 0000000000..294fde015e
--- /dev/null
+++ b/examples/aws-redis-serverless/sst-env.d.ts
@@ -0,0 +1,16 @@
+/* This file is auto-generated by SST. Do not edit. */
+/* tslint:disable */
+/* eslint-disable */
+/* deno-fmt-ignore-file */
+
+declare module "sst" {
+ export interface Resource {
+ "MyVpc": {
+ "type": "sst.aws.Vpc"
+ }
+ }
+}
+///
+
+import "sst"
+export {}
\ No newline at end of file
diff --git a/examples/aws-redis-serverless/sst.config.ts b/examples/aws-redis-serverless/sst.config.ts
new file mode 100644
index 0000000000..66ca9dce65
--- /dev/null
+++ b/examples/aws-redis-serverless/sst.config.ts
@@ -0,0 +1,28 @@
+///
+
+export default $config({
+ app(input) {
+ return {
+ name: "aws-redis-serverless",
+ removal: input?.stage === "production" ? "retain" : "remove",
+ home: "aws",
+ };
+ },
+ async run() {
+ // Serverless Redis doesn't require NAT Gateways
+ const vpc = new sst.aws.Vpc("MyVpc");
+ const redis = new sst.aws.Redis("MyRedis", {
+ vpc,
+ serverless: {
+ dataStorage: { maximum: 10, unit: "GB" },
+ ecpuPerSeconds: { maximum: 5000 }
+ }
+ });
+ new sst.aws.Function("MyApp", {
+ handler: "index.handler",
+ url: true,
+ vpc,
+ link: [redis],
+ });
+ },
+});
diff --git a/platform/src/components/aws/redis.ts b/platform/src/components/aws/redis.ts
index c690f39270..ef804452fe 100644
--- a/platform/src/components/aws/redis.ts
+++ b/platform/src/components/aws/redis.ts
@@ -102,6 +102,65 @@ export interface RedisArgs {
* ```
*/
parameters?: Input>>;
+ /**
+ * Enable serverless Redis using Amazon ElastiCache Serverless.
+ *
+ * Serverless Redis automatically scales based on usage and offers pay-per-use pricing.
+ * When enabled, traditional cluster configuration options like `instance`, `cluster`, and `parameters` are ignored.
+ *
+ * @default `false`
+ * @example
+ * Enable serverless Redis with default limits.
+ * ```js
+ * {
+ * serverless: true
+ * }
+ * ```
+ *
+ * Configure serverless Redis with custom limits.
+ * ```js
+ * {
+ * serverless: {
+ * dataStorage: { maximum: 100, unit: "GB" },
+ * ecpuPerSeconds: { maximum: 50000 }
+ * }
+ * }
+ * ```
+ */
+ serverless?: Input<
+ | boolean
+ | {
+ /**
+ * The maximum data storage limit in GB.
+ *
+ * @default `{ maximum: 10, unit: "GB" }`
+ * @example
+ * ```js
+ * {
+ * dataStorage: { maximum: 100, unit: "GB" }
+ * }
+ * ```
+ */
+ dataStorage?: Input<{
+ maximum: Input;
+ unit: Input<"GB">;
+ }>;
+ /**
+ * The maximum ElastiCache Processing Units (ECPU) per second.
+ *
+ * @default `{ maximum: 5000 }`
+ * @example
+ * ```js
+ * {
+ * ecpuPerSeconds: { maximum: 50000 }
+ * }
+ * ```
+ */
+ ecpuPerSeconds?: Input<{
+ maximum: Input;
+ }>;
+ }
+ >;
/**
* The VPC to use for the Redis instance.
*
@@ -209,6 +268,10 @@ export interface RedisArgs {
* Transform the Redis cluster.
*/
cluster?: Transform;
+ /**
+ * Transform the Redis serverless cache.
+ */
+ serverlessCache?: Transform;
};
}
@@ -230,6 +293,30 @@ interface RedisRef {
* const redis = new sst.aws.Redis("MyRedis", { vpc });
* ```
*
+ * #### Create a serverless Redis
+ *
+ * You can also create a serverless Redis instance that automatically scales based on usage.
+ *
+ * ```js title="sst.config.ts"
+ * const vpc = new sst.aws.Vpc("MyVpc");
+ * const redis = new sst.aws.Redis("MyRedis", {
+ * vpc,
+ * serverless: true
+ * });
+ * ```
+ *
+ * Configure serverless limits.
+ *
+ * ```js title="sst.config.ts"
+ * const redis = new sst.aws.Redis("MyRedis", {
+ * vpc,
+ * serverless: {
+ * dataStorage: { maximum: 100, unit: "GB" },
+ * ecpuPerSeconds: { maximum: 50000 }
+ * }
+ * });
+ * ```
+ *
* #### Link to a resource
*
* You can link your cluster to other resources, like a function or your Next.js app.
@@ -307,6 +394,7 @@ interface RedisRef {
*/
export class Redis extends Component implements Link.Linkable {
private cluster?: Output;
+ private serverlessCache?: Output;
private _authToken?: Output;
private dev?: {
enabled: boolean;
@@ -334,9 +422,6 @@ export class Redis extends Component implements Link.Linkable {
const version = all([engine, args.version]).apply(
([engine, v]) => v ?? (engine === "redis" ? "7.1" : "7.2"),
);
- const instance = output(args.instance).apply((v) => v ?? "t4g.micro");
- const argsCluster = normalizeCluster();
- const vpc = normalizeVpc();
const dev = registerDev();
if (dev?.enabled) {
@@ -344,13 +429,24 @@ export class Redis extends Component implements Link.Linkable {
return;
}
+ const vpc = normalizeVpc();
+ const serverless = normalizeServerless();
+ if (serverless.enabled) {
+ const serverlessCache = createServerlessCache();
+ this.serverlessCache = serverlessCache;
+ return;
+ }
+ const instance = output(args.instance).apply((v) => v ?? "t4g.micro");
+ const argsCluster = normalizeCluster();
+
const { authToken, secret } = createAuthToken();
+ this._authToken = authToken;
+
const subnetGroup = createSubnetGroup();
const parameterGroup = createParameterGroup();
const cluster = createCluster();
this.cluster = cluster;
- this._authToken = authToken;
function reference() {
const ref = args as unknown as RedisRef;
@@ -460,6 +556,26 @@ Listening on "${dev.host}:${dev.port}"...`,
});
}
+ function normalizeServerless() {
+ return output(args.serverless).apply((v) => {
+ if (v === true) {
+ return {
+ enabled: true,
+ dataStorage: { maximum: 10, unit: "GB" },
+ ecpuPerSeconds: { maximum: 5000 },
+ };
+ }
+ if (v === false || v === undefined) {
+ return { enabled: false };
+ }
+ return {
+ enabled: true,
+ dataStorage: v.dataStorage ?? { maximum: 10, unit: "GB" },
+ ecpuPerSeconds: v.ecpuPerSeconds ?? { maximum: 5000 },
+ };
+ });
+ }
+
function createAuthToken() {
const authToken = new RandomPassword(
`${name}AuthToken`,
@@ -585,13 +701,54 @@ Listening on "${dev.host}:${dev.port}"...`,
),
);
}
+
+ function createServerlessCache() {
+ return serverless.apply(
+ (serverless) =>
+ new elasticache.ServerlessCache(
+ ...transform(
+ args.transform?.serverlessCache,
+ `${name}ServerlessCache`,
+ {
+ description: "Managed by SST",
+ engine,
+ name: `${name.toLowerCase()}`,
+ majorEngineVersion: version.apply((v) => {
+ // Extract major version (e.g., "7.1" -> "7")
+ const majorVersion = v.split(".")[0];
+ return majorVersion;
+ }),
+ cacheUsageLimits: {
+ dataStorage: {
+ maximum: serverless.dataStorage?.maximum ?? 10,
+ unit: serverless.dataStorage?.unit ?? "GB",
+ },
+ ecpuPerSeconds: [
+ {
+ maximum: serverless.ecpuPerSeconds?.maximum ?? 5000,
+ },
+ ],
+ },
+ securityGroupIds: vpc.securityGroups,
+ subnetIds: vpc.subnets,
+ tags: {
+ "sst:component-version": _version.toString(),
+ },
+ },
+ { parent: self },
+ ),
+ ),
+ );
+ }
}
/**
* The ID of the Redis cluster.
*/
public get clusterId() {
- return this.dev ? output("placeholder") : this.cluster!.id;
+ if (this.dev) return output("placeholder");
+ if (this.serverlessCache) return this.serverlessCache.id;
+ return this.cluster!.id;
}
/**
@@ -612,20 +769,30 @@ Listening on "${dev.host}:${dev.port}"...`,
* The host to connect to the Redis cluster.
*/
public get host() {
- return this.dev
- ? this.dev.host
- : this.cluster!.clusterEnabled.apply((enabled) =>
- enabled
- ? this.cluster!.configurationEndpointAddress
- : this.cluster!.primaryEndpointAddress,
- );
+ if (this.dev) return this.dev.host;
+ if (this.serverlessCache) {
+ return this.serverlessCache.endpoints.apply(
+ (endpoints) => endpoints?.[0]?.address ?? "",
+ );
+ }
+ return this.cluster!.clusterEnabled.apply((enabled) =>
+ enabled
+ ? this.cluster!.configurationEndpointAddress
+ : this.cluster!.primaryEndpointAddress,
+ );
}
/**
* The port to connect to the Redis cluster.
*/
public get port() {
- return this.dev ? this.dev.port : this.cluster!.port.apply((v) => v!);
+ if (this.dev) return this.dev.port;
+ if (this.serverlessCache) {
+ return this.serverlessCache.endpoints.apply(
+ (endpoints) => endpoints?.[0]?.port ?? 6379,
+ );
+ }
+ return this.cluster!.port.apply((v) => v!);
}
/**
@@ -642,6 +809,16 @@ Listening on "${dev.host}:${dev.port}"...`,
throw new VisibleError("Cannot access `nodes.cluster` in dev mode.");
return _this.cluster!;
},
+ /**
+ * The ElastiCache Redis serverless cache.
+ */
+ get serverlessCache() {
+ if (_this.dev)
+ throw new VisibleError(
+ "Cannot access `nodes.serverlessCache` in dev mode.",
+ );
+ return _this.serverlessCache!;
+ },
};
}
diff --git a/platform/src/components/component.ts b/platform/src/components/component.ts
index 24bedb419b..68507ca853 100644
--- a/platform/src/components/component.ts
+++ b/platform/src/components/component.ts
@@ -233,6 +233,11 @@ export class Component extends ComponentResource {
40,
{ lower: true, replace: (name) => name.replaceAll(/-+/g, "-") },
],
+ "aws:elasticache/serverlessCache:ServerlessCache": [
+ "name",
+ 40,
+ { lower: true, replace: (name) => name.replaceAll(/-+/g, "-") },
+ ],
"aws:elasticache/subnetGroup:SubnetGroup": [
"name",
255,