Skip to content

Fix: SST Not Working — Deploy Failing, Bindings Not Linking, or Lambda Functions Timing Out

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

Quick Answer

How to fix SST (Serverless Stack) issues — resource configuration with sst.config.ts, linking resources to functions, local dev with sst dev, database and storage setup, and deployment troubleshooting.

The Problem

sst dev starts but the linked resource is undefined:

import { Resource } from 'sst';

export async function handler() {
  const bucketName = Resource.MyBucket.name;
  // Error: Cannot read properties of undefined (reading 'name')
}

Or deployment fails with an AWS error:

npx sst deploy --stage production
# Error: User: arn:aws:iam::123456:user/dev is not authorized to perform: cloudformation:CreateStack

Or the function deploys but times out:

Task timed out after 10.00 seconds

Or sst dev can’t connect to the live environment:

Error: Could not connect to the IoT endpoint

Why This Happens

SST (Ion) is an Infrastructure-as-Code framework that deploys to AWS using Pulumi under the hood. It provides a streamlined developer experience for building full-stack serverless apps, and almost every failure traces back to one of four root causes. Resources must be linked to functions: SST’s link property connects infrastructure resources (buckets, databases, queues) to Lambda functions. Without linking, the function has no IAM permissions and no environment variables for the resource. The Resource import reads linked values from environment variables set at deploy time, so an unlinked resource shows up as undefined at runtime with no compile-time warning.

AWS credentials must have sufficient permissions. SST creates CloudFormation stacks, S3 buckets, Lambda functions, API Gateway endpoints, IAM roles, and more. The deploying user needs broad IAM permissions — restricted IAM users get authorization errors. sst dev uses AWS IoT Core for live Lambda invocations: SST routes Lambda calls to your local machine through an IoT WebSocket topic. This requires IoT permissions and stable connectivity. Firewalls or VPNs can block the WebSocket connection. Lambda also defaults to a short 10-second timeout and 1024MB memory in SST’s defaults. Long-running operations (database migrations, file processing) need higher limits explicitly configured per route.

A subtler failure mode is mismatched stack state: if a previous deploy was interrupted (Ctrl+C, network drop, CI killed), the Pulumi state file can be out of sync with what’s actually in AWS. The next sst deploy will refuse to proceed because it thinks resources exist that don’t, or vice versa. The fix is almost always npx sst unlock followed by npx sst refresh to reconcile state, but only after confirming you understand what’s drifted.

Platform and Environment Differences

AWS region availability drives more SST failures than any other variable. Not every region has every Lambda runtime, not every region has Aurora Serverless v2, and us-east-1 is the only region that hosts certain global services (CloudFront certificates, IAM). If your sst.config.ts targets ap-northeast-3 (Osaka) but uses sst.aws.Postgres, the deploy fails because Aurora Serverless v2 was not available there at launch — verify per-region service availability before picking a non-standard region. Bedrock-backed AI features only work in regions where the model is enabled, so a working us-west-2 config will silently 404 when redeployed to eu-central-1.

Local dev versus deployed behaviour diverges because sst dev runs your handler in your local Node.js process while the deployed Lambda runs in AWS’s Linux container with the configured runtime. Modules that work locally on macOS arm64 may fail in Lambda’s arm64 runtime if they ship pre-compiled binaries for the wrong target. Sharp, canvas, and any module with .node files are the usual offenders — set architecture: 'arm64' consistently and rebuild with --target_arch=arm64. sst dev also doesn’t enforce Lambda’s read-only filesystem outside /tmp, so writes to process.cwd() silently work locally and explode in production.

Monorepo integration with Turborepo or Nx changes how SST’s file watcher behaves. SST expects to find sst.config.ts at the repo root, but in a Turborepo workspace the actual Lambda source lives at apps/api/src/. You must set the handler path correctly relative to the repo root and add SST’s output directory to .turbo cache exclusions, or stale .sst/ artifacts will be cached across CI jobs and cause Resource is undefined errors. Nx users should add outputs: ["{workspaceRoot}/.sst/**"] to the deploy target.

SST v2 versus v3 (Ion) is the biggest migration surface. SST v2 used AWS CDK and shipped constructs like Api, Bucket, Table. SST v3 (Ion) uses Pulumi and renames everything to sst.aws.ApiGatewayV2, sst.aws.Bucket, sst.aws.Dynamo. The bind array became link, permissions is auto-derived, and stacks/ directory is gone — everything lives in sst.config.ts. Trying to run v2 code on v3 produces baffling “Function not found” errors because the construct names don’t exist anymore. If you’re on v2, follow the Pulumi migration path before bug-hunting individual errors.

Fix 1: Configure sst.config.ts

npx sst@latest init
// sst.config.ts — SST Ion configuration
export default $config({
  app(input) {
    return {
      name: 'my-app',
      removal: input?.stage === 'production' ? 'retain' : 'remove',
      home: 'aws',
      providers: {
        aws: {
          region: 'us-east-1',
        },
      },
    };
  },
  async run() {
    // S3 Bucket
    const bucket = new sst.aws.Bucket('MyBucket', {
      access: 'public',  // Public read access
    });

    // DynamoDB Table
    const table = new sst.aws.Dynamo('MyTable', {
      fields: {
        pk: 'string',
        sk: 'string',
        gsi1pk: 'string',
        gsi1sk: 'string',
      },
      primaryIndex: { hashKey: 'pk', rangeKey: 'sk' },
      globalIndexes: {
        gsi1: { hashKey: 'gsi1pk', rangeKey: 'gsi1sk' },
      },
    });

    // Secret values
    const dbUrl = new sst.Secret('DatabaseUrl');
    const apiKey = new sst.Secret('ApiKey');

    // API with linked resources
    const api = new sst.aws.ApiGatewayV2('MyApi');

    api.route('GET /users', {
      handler: 'src/functions/users.list',
      link: [table, dbUrl],  // Link resources to this function
    });

    api.route('POST /upload', {
      handler: 'src/functions/upload.handler',
      link: [bucket, apiKey],
      timeout: '30 seconds',
      memory: '512 MB',
    });

    // Next.js frontend
    const site = new sst.aws.Nextjs('MySite', {
      link: [api, bucket, table],
      environment: {
        NEXT_PUBLIC_API_URL: api.url,
      },
    });

    return {
      api: api.url,
      site: site.url,
      bucket: bucket.name,
    };
  },
});
// src/functions/users.ts — access linked resources
import { Resource } from 'sst';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, QueryCommand, PutCommand } from '@aws-sdk/lib-dynamodb';

const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));

export async function list() {
  // Resource.MyTable.name is available because of the `link` property
  const result = await client.send(new QueryCommand({
    TableName: Resource.MyTable.name,
    KeyConditionExpression: 'pk = :pk',
    ExpressionAttributeValues: { ':pk': 'USER' },
  }));

  return {
    statusCode: 200,
    body: JSON.stringify(result.Items),
  };
}

export async function create(event: any) {
  const body = JSON.parse(event.body);

  await client.send(new PutCommand({
    TableName: Resource.MyTable.name,
    Item: {
      pk: 'USER',
      sk: `USER#${body.id}`,
      name: body.name,
      email: body.email,
      createdAt: new Date().toISOString(),
    },
  }));

  return { statusCode: 201, body: JSON.stringify({ created: true }) };
}
// src/functions/upload.ts — S3 upload
import { Resource } from 'sst';
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({});

export async function handler(event: any) {
  const { filename, contentType } = JSON.parse(event.body);

  // Generate pre-signed upload URL
  const command = new PutObjectCommand({
    Bucket: Resource.MyBucket.name,
    Key: `uploads/${filename}`,
    ContentType: contentType,
  });

  const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 3600 });

  return {
    statusCode: 200,
    body: JSON.stringify({ uploadUrl }),
  };
}
// Access secrets
import { Resource } from 'sst';

const dbUrl = Resource.DatabaseUrl.value;  // Secret value
const apiKey = Resource.ApiKey.value;
# Set secret values
npx sst secret set DatabaseUrl "postgres://user:pass@host/db"
npx sst secret set ApiKey "sk_live_abc123"

# Set per stage
npx sst secret set DatabaseUrl "postgres://..." --stage production

Fix 3: Local Development with sst dev

# Start local development
npx sst dev

# This:
# 1. Deploys infrastructure to AWS (real DynamoDB, S3, etc.)
# 2. Routes Lambda invocations to your local machine
# 3. Watches for code changes and hot-reloads

# Start with a specific stage
npx sst dev --stage dev

# With a specific AWS profile
npx sst dev --profile my-aws-profile
// sst.config.ts — dev-specific configuration
export default $config({
  async run() {
    const isProd = $app.stage === 'production';

    const table = new sst.aws.Dynamo('MyTable', {
      fields: { pk: 'string', sk: 'string' },
      primaryIndex: { hashKey: 'pk', rangeKey: 'sk' },
      // Remove on non-prod stage deletion
      transform: {
        table: {
          deletionProtection: isProd,
        },
      },
    });

    // Different config per stage
    const api = new sst.aws.ApiGatewayV2('MyApi');
    api.route('GET /health', 'src/functions/health.handler');

    // Custom domain in production
    if (isProd) {
      api.addRoute('GET /users', {
        handler: 'src/functions/users.list',
        link: [table],
      });
    }
  },
});

Fix 4: Database Integration

// sst.config.ts — RDS (Postgres or MySQL)
export default $config({
  async run() {
    const vpc = new sst.aws.Vpc('MyVpc');

    const database = new sst.aws.Postgres('MyDatabase', {
      vpc,
      scaling: {
        min: '0.5 ACU',   // Scale to zero when idle
        max: '4 ACU',
      },
    });

    const api = new sst.aws.ApiGatewayV2('MyApi');
    api.route('GET /users', {
      handler: 'src/functions/users.list',
      link: [database],
      vpc,  // Function must be in the same VPC
    });
  },
});
// src/functions/users.ts — access RDS
import { Resource } from 'sst';
import { drizzle } from 'drizzle-orm/aws-data-api/pg';
import { RDSDataClient } from '@aws-sdk/client-rds-data';
import * as schema from '../db/schema';

const client = new RDSDataClient({});

const db = drizzle(client, {
  database: Resource.MyDatabase.database,
  secretArn: Resource.MyDatabase.secretArn,
  resourceArn: Resource.MyDatabase.clusterArn,
  schema,
});

export async function list() {
  const users = await db.select().from(schema.users);
  return {
    statusCode: 200,
    body: JSON.stringify(users),
  };
}

Fix 5: Queues and Cron Jobs

// sst.config.ts
export default $config({
  async run() {
    // SQS Queue with subscriber
    const queue = new sst.aws.Queue('MyQueue');
    queue.subscribe('src/functions/worker.handler', {
      link: [table],
      timeout: '5 minutes',
    });

    // API route that publishes to queue
    const api = new sst.aws.ApiGatewayV2('MyApi');
    api.route('POST /jobs', {
      handler: 'src/functions/enqueue.handler',
      link: [queue],
    });

    // Cron job
    new sst.aws.Cron('DailyReport', {
      schedule: 'rate(1 day)',  // Or: cron(0 9 * * ? *)
      job: {
        handler: 'src/functions/report.handler',
        link: [table],
        timeout: '5 minutes',
      },
    });
  },
});
// src/functions/enqueue.ts — publish to queue
import { Resource } from 'sst';
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';

const sqs = new SQSClient({});

export async function handler(event: any) {
  const body = JSON.parse(event.body);

  await sqs.send(new SendMessageCommand({
    QueueUrl: Resource.MyQueue.url,
    MessageBody: JSON.stringify({
      type: 'process-image',
      imageKey: body.imageKey,
    }),
  }));

  return { statusCode: 202, body: JSON.stringify({ queued: true }) };
}

// src/functions/worker.ts — process queue messages
export async function handler(event: any) {
  for (const record of event.Records) {
    const message = JSON.parse(record.body);
    console.log('Processing:', message.type, message.imageKey);
    await processImage(message.imageKey);
  }
}

Fix 6: Deploy to Production

# Deploy to production
npx sst deploy --stage production

# Deploy with specific profile
npx sst deploy --stage production --profile prod-aws

# Remove a stage (deletes all resources)
npx sst remove --stage dev

# View outputs (URLs, resource names)
npx sst output --stage production

# Open the SST console (web dashboard)
npx sst console

CI/CD deployment (GitHub Actions):

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx sst deploy --stage production
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Still Not Working?

Resource.X is undefined — the resource isn’t linked to the function. Add it to the link array: link: [bucket, table]. Every resource accessed via Resource.* must be explicitly linked. Linking sets environment variables and IAM permissions automatically.

“User is not authorized” on deploy — SST needs broad IAM permissions to create CloudFormation stacks, Lambda functions, API Gateway, S3, DynamoDB, etc. For initial setup, use an IAM user with AdministratorAccess. For production, create a scoped policy based on the resources SST creates.

sst dev hangs or can’t connect — SST uses AWS IoT Core for live Lambda. Ensure your AWS credentials are valid and have IoT permissions. VPNs or corporate firewalls often block WebSocket connections to IoT endpoints. Try disconnecting from VPN.

Lambda timeout at 10 seconds — increase the timeout in the route or function config: timeout: '60 seconds'. For long-running tasks, use a queue pattern instead — publish to SQS and process asynchronously with a higher timeout subscriber.

Deploy fails with “state already locked” or “concurrent update in progress” — a previous deploy crashed before releasing the Pulumi state lock. Run npx sst unlock --stage <name> to release it. If state is also drifted from reality, follow up with npx sst refresh --stage <name> to reconcile. Never delete the .sst/ directory manually — that orphans real AWS resources from the state file and you’ll end up paying for resources you can no longer manage.

Function works in sst dev but the deployed version returns 500 — the local dev shim doesn’t enforce Lambda’s read-only filesystem, missing native binaries, or 250MB unzipped size limit. Check CloudWatch Logs for the real error. Common causes: writing to process.cwd() (use /tmp instead), missing sharp arm64 binary (rebuild with npm rebuild sharp --arch=arm64 --platform=linux), or the deployment bundle exceeded the size limit (use Lambda layers or container images).

Bedrock or other AI resource throws “service not available” — your stack region doesn’t have the model enabled, or the IAM role doesn’t include bedrock:InvokeModel permission. Bedrock model access is opt-in per region in the AWS console. Switch to us-east-1 or us-west-2 for the broadest model availability, and add the permission explicitly with permissions: [{ actions: ['bedrock:InvokeModel'], resources: ['*'] }] on the route.

For related serverless issues, see Fix: Wrangler Not Working, Fix: Inngest Not Working, Fix: AWS Lambda Timeout, and Fix: Pulumi Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles