@alessandrolattao/sst
v4.17.1-alessandrolattao.29
Published
SST CLI fork with a parallel Go build pipeline
Readme
A personal fork of SST that makes Go projects build faster and behave properly in dev mode.
Everything SST does, it still does. The changes live entirely in the Go runtime and in the dev rebuild loop, and they matter most on a monorepo with many handlers: this fork was built against one with 438 Go Lambda functions.
What is different
Go builds run in parallel. The Go runtime held an exclusive lock around the whole build, so it compiled one function at a time no matter how many cores were available. The lock now guards only its own bookkeeping.
SST_BUILD_CONCURRENCY_FUNCTIONworks for Go. The documented knob was implemented for Node and Python only, and silently ignored for Go handlers. It now caps Go builds too, at 4 by default.Rebuilds follow real imports. In dev, SST decided what to rebuild from the directory a changed file sits in. It now uses the handler's actual import graph, so editing a shared package rebuilds every function that imports it, and nothing else.
Invalidated handlers rebuild together. Once rebuild scope follows imports, one save can invalidate hundreds of handlers. They used to be recompiled one after another; now they are compiled concurrently, under a limit.
A deploy stops recompiling handlers one at a time. Every deploy drops the dev build cache, and each worker then restarts and compiles whatever it no longer finds there, one after another on the event loop. On a stage with 18 Go handlers in dev that was 50 seconds of wall clock, for source that had not changed. The same set now goes through the concurrent rebuild above, and handlers whose runtime starts lazily are left to compile when something invokes them.
Editing a shared package no longer redeploys every function. Go stamps each binary with a hash of every source file that fed the build, including the ones the linker then discards as unreachable. Touch one file in a package that hundreds of handlers import and every artifact gets a new hash, so the deploy uploads all of them and updates every Function, for programs whose code is byte-identical to what is already deployed. Deploy builds now link with
-buildid=, so two builds differ when the program differs and not before. What that gives up isNT_GNU_BUILD_ID, which-s -whad already made useless here.Deploys stop paying for dev-only work. The import graph is only ever read by the dev file watcher, but it was captured after every build, roughly doubling the cost of every deploy build. It is now captured only in dev.
Every Go build says how long it took. A deploy printed nothing between asking for a build and the Function resource turning up minutes later, so the compile time of a single handler was only visible by reading the debug log. Each finished build now prints a
Builtline with its handler and its seconds, in the same shape as theCreated/Updatedlines that already carry a duration. It rides on the one event SST's UI prints verbatim, so this stays a change to the Go runtime: no new event type, no case added to the printer, nothing for an upstream merge to land on.Smaller fixes. Resolving
GOMODCACHEis cancellable instead of outliving a Ctrl-C,go listoutput is decoded through explicit JSON tags, and an unparsable concurrency value warns and keeps the default instead of quietly falling back to serial builds.
What it costs
Measured on the 438-handler monorepo this was written for, on a no-op deploy where nothing changed:
| | before | after | |---|---|---| | build concurrency | 1 (peak 2) | up to the configured limit | | per-handler build | ~1.4s, of which half was the dev-only graph capture | ~0.6s |
And on a deploy that did change something: one file edited in the shared handler framework, which 434 of those modules import. Before, all 434 were uploaded again and had their Function updated. After, only the handlers whose linked program actually changed, which for that edit was one.
Installation
npm install @alessandrolattao/sst
# bun add @alessandrolattao/sstBuilt for Linux (x64 and arm64) and macOS (Apple silicon and Intel). The binary is still called sst, so bunx sst, npx sst and every existing script keep working unchanged.
Versions are published as pre-releases (4.17.1-alessandrolattao.1), which means they must be pinned explicitly and will never be picked up by a ^4 range by accident. If your sst.config.ts declares a version constraint, it needs a pre-release floor to accept one, since SST checks it with Masterminds/semver where a plain >= never matches a pre-release:
version: ">= 4.13.1-0",Starting a new project
sst init writes an sst.config.ts for whatever it finds in the current
directory, so install the CLI first and let it generate the config:
mkdir my-app && cd my-app
npm init -y
npm install @alessandrolattao/[email protected]
npx sst initThen open the generated sst.config.ts and add the pre-release floor to its
app() return, otherwise the CLI refuses to run against its own config:
export default $config({
app(input) {
return {
name: "my-app",
version: ">= 4.13.1-0",
home: "aws",
};
},
async run() {},
});From here everything is upstream SST: npx sst deploy --stage dev,
npx sst dev, and the getting started guides
apply unchanged.
Relationship with upstream
This fork tracks sst/sst and is kept in sync with it automatically: a daily job rebases the patches onto each new upstream release, verifies the result builds and passes, and publishes a matching build. When a rebase conflicts it stops and opens an issue instead of publishing something nobody read. Branches:
dev— a plain mirror of upstream, never committed to directlyfeat/…— one branch per change, rebased ondev, each self-contained enough to be sent upstreamalessandrolattao— what actually gets built and published: upstream plus the patches plus the fork's own naming
The published package is renamed so it can sit next to the real one without claiming its name. Everything else, including the SST_BIN_PATH escape hatch, behaves exactly as upstream.
Upstream documentation
The fork changes no behaviour you interact with, so upstream's docs apply as they are:
Running locally
Run bun run setup. You need Go and Bun installed.
cd examples/aws-api
go run ../../cmd/sst <command>To try a build of this fork against a real project without publishing anything, point SST_BIN_PATH at it:
go build -o /tmp/sst ./cmd/sst
SST_BIN_PATH=/tmp/sst bunx sst deploy --stage devLicense
MIT, same as upstream. See LICENSE, which keeps the original SST copyright.
