all posts

The best Rust hosting platforms in 2026

Ajay Kumar··11 min read

Hosting guides for other languages are about the runtime: which Node version, which Python, how the interpreter starts, how big node_modules got. Rust inverts the whole exercise. The thing you eventually ship — one mostly-static binary that starts in milliseconds and idles on very little memory — is the most boring, most portable artifact in modern software. Getting to that binary is the part that will ruin your afternoon.

Which is why the single most useful question to ask a Rust host is not about its runtime at all. It's: how much RAM does your build machine have, and does anything survive between builds? The borrow checker being satisfied is a poor predictor of the CI builder being satisfied. Rust's most feared error message isn't emitted by rustc — it's `signal: 9, SIGKILL` after eight minutes of clean compilation, which is the operating system's way of announcing that LLVM won.

This is a buyer's guide for putting an Axum, Actix Web, Rocket, Loco, or Leptos/Dioxus SSR service into production. I build PandaStack, which appears near the bottom — flagged, with its trade-offs rather than its brochure copy.

Everything about third-party platforms below is deliberately qualitative — no prices, no tier limits, no build-time or cold-start numbers. Builder sizes, caching behaviour, and pricing all move fast enough that any figure written into a blog post is wrong within a quarter. Verify against each vendor's current docs before committing an application to one.

Rust deploys are shaped oddly, in a specific way

The build is the expensive half

Cargo compiles your entire dependency graph from source. There is no equivalent of a prebuilt npm tarball or a Python wheel: every crate you depend on, and every crate they depend on, is compiled on your builder, by your compiler, at your optimisation level. Generics are monomorphised, so one generic function used with eight types becomes eight functions of real machine code, and a dependency tree with heavy generic use — which is most of the async ecosystem — produces a genuinely large amount of LLVM IR to chew through.

The memory profile is what makes this a hosting problem rather than a patience problem. Compiling many crates in parallel means many rustc processes, each with its own LLVM instance, each holding its own working set. Then, if you enabled fat link-time optimisation the way every "optimise your Rust binary" post recommends, the very last step merges all of it into a single LLVM module and optimises that. Peak memory therefore arrives at the end, after all the cheap parallel work is finished — which is exactly why the build dies at minute eight instead of failing fast at minute one, and why the failure looks like a platform fault rather than a configuration choice you made.

Disk is the quieter cost. A `target/` directory for a moderate workspace holds object files, incremental artefacts, and multiple copies of dependencies compiled under different feature flags, and it gets large enough to matter on a builder with a small ephemeral volume. If a platform's build step fails with a disk error rather than a memory one, that's usually the answer.

Caching, and why naive Docker caching doesn't do it

The obvious Dockerfile — `COPY . .` then `cargo build --release` — throws away the entire dependency build every time you change one line of your own code, because the layer's inputs changed. That's the difference between a deploy you can iterate on and a deploy you go make coffee during.

There are three tools worth knowing, and they compose. `cargo-chef` computes a dependency-only "recipe" from your manifests and builds it in its own layer, so that layer invalidates only when `Cargo.toml` or `Cargo.lock` changes. BuildKit cache mounts keep the crates.io registry and, optionally, `target/` on the builder between runs. And `sccache` puts a shared compilation cache behind all of it, which is what you want when builds happen on ephemeral machines that never see the same disk twice — it can be backed by object storage, so a fresh builder still gets warm hits.

The question to ask a platform is narrower than "do you cache?" — it's whether the cache survives between deploys, whether it's shared across branches or isolated per branch, and whether it survives a builder being recycled. A platform that caches within a build and discards it afterwards has not solved your problem.

# syntax=docker/dockerfile:1.7
# Pin the toolchain once, in rust-toolchain.toml, so CI, prod, and your
# laptop agree on the compiler. Cheapest reproducibility win in the language.
FROM rust:1-bookworm AS chef
RUN cargo install cargo-chef --locked
WORKDIR /app

# Reduce the workspace to a dependency-only "recipe". This depends on the
# manifests only -- editing src/ does not change it.
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json

# Build ONLY the dependencies. This is the eight-minute layer. It now gets
# reused by every commit that doesn't touch Cargo.toml or Cargo.lock.
FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/app/target \
    cargo chef cook --release --recipe-path recipe.json

# Now your crate, and only your crate. Seconds, not minutes.
COPY . .
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/app/target \
    cargo build --release --locked --bin server \
    && cp target/release/server /usr/local/bin/server

# Ship the binary, not the toolchain that produced it.
FROM debian:bookworm-slim AS runtime
RUN apt-get update && apt-get install -y --no-install-recommends \
      ca-certificates libssl3 && rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/bin/server /usr/local/bin/server
ENV PORT=8080
EXPOSE 8080
CMD ["/usr/local/bin/server"]
The `--mount=type=cache,target=/app/target` line only helps if the platform's builder actually persists BuildKit cache mounts between deploys. Several hosted builders honour the syntax and quietly start from empty every time, which produces a Dockerfile that looks optimised and behaves like it isn't. Time two consecutive deploys with no source change: if the second isn't dramatically faster, the cache isn't real.

The OOM, and how to actually stop it

When a release build gets killed, people reach for the wrong lever first. The effective fixes, roughly in order of how much they cost you: cap the number of parallel jobs, switch fat LTO to thin, reduce codegen units, drop debug info from the release profile, and only then buy a bigger builder. Capping `-j` is the one that costs nothing but wall-clock time and fixes most cases, because it directly bounds how many LLVM instances exist at once.

# Cargo.toml -- the release profile is where build cost, peak memory, and
# binary size are all actually decided.

[profile.release]
# "fat" LTO merges every crate into one LLVM module at the very END of the
# build. It gives the best binary and it is also the single most likely step
# to hand your builder to the OOM killer. "thin" is close in output quality
# and dramatically kinder to memory.
lto = "thin"
# Fewer codegen units: better optimisation, less parallelism, lower peak RAM.
codegen-units = 4
opt-level = 3
strip = "symbols"    # no separate strip step, much smaller artifact
panic = "abort"      # skips unwinding tables -- NOT if you rely on catch_unwind
incremental = false  # useless in CI; it only grows target/

# A cheaper profile for preview environments and CI checks: still optimised
# enough to be representative, but it finishes while you're still watching.
[profile.ci]
inherits = "release"
lto = false
codegen-units = 16
debug = false

# On a memory-constrained builder, cap parallel rustc/LLVM jobs. This is the
# most effective OOM fix that does not involve buying RAM:
#
#   cargo build --release --locked -j 2
#
# And if a dependency needs a C toolchain (ring, openssl-sys, libz-ng-sys),
# confirm the build image has one before blaming the platform.

The output, by contrast, is trivial to run

Once the binary exists, Rust is the easiest guest a platform ever hosts. There's no interpreter to start, no bytecode to load, no dependency tree to import at boot, and no garbage collector to pause you at an inconvenient moment. Process startup is essentially the dynamic linker resolving a handful of libraries — and if you target musl or a fully static build, not even that. Idle memory is small enough that the platform's minimum instance size is usually the binding constraint, not your app.

This has a real consequence for how you read platform marketing. When a host advertises cold-start improvements, almost none of that is about your language for Rust: the cold start you pay is the platform's — pulling a container image, booting a machine, attaching a network — not the process starting. A Rust service is the case where the substrate's own latency is fully exposed, with nothing to hide behind.

Rust services tend to be long-running, which rules some things out

People reach for Rust when a process is going to be alive and busy: a WebSocket gateway, a streaming API, a job runner, something holding a connection pool and an in-memory cache. Axum and Actix Web apps routinely have `tokio::spawn` loops doing periodic work, a `sqlx` pool sized deliberately, and background tasks that assume the runtime still exists a minute from now. Freeze the environment between invocations and all of that stops: the timer doesn't fire, the flush task doesn't flush, and the pool's idle connections rot.

That's the structural argument against request-scoped platforms — not performance, which Rust wins comfortably. It's that half of what your service does happens between requests. There's a broader treatment of that trade-off at /blog/how-to-move-from-serverless-functions-to-a-long-running-server, and the connection-lifetime version at /blog/websocket-apps-persistent-connections-hosting.

What to actually evaluate

  • Build resources — how much RAM and how many cores the builder has, and whether you can pick a bigger one. This is the axis that decides whether you can deploy at all.
  • Build caching — registry cache, target cache, sccache. Does it persist between deploys, is it per-branch or shared, and does it survive a recycled builder?
  • Build timeout — a cold Rust build on a fresh machine is not fast, and some platforms have a ceiling you'll meet on your first deploy.
  • Dockerfile required or not — whether the platform can detect a Cargo project and build it, or expects you to own the image. Neither is wrong; know which you're signing up for.
  • Target architecture — arm64 versus x86_64, glibc versus musl. Cross-compiling to the platform's arch is a solved problem, but it is a problem you have to notice.
  • Cold start and scale-to-zero — how long from zero instances to serving, and whether idle costs you anything. Rust's own start is negligible, so this is entirely the platform's number.
  • Long-lived process support — WebSockets end to end, CPU allocated between requests, and a proxy that doesn't cap connection duration.
  • Managed Postgres — whether the database is a first-class object in the same region, or a separate vendor and a separate latency budget.
  • Deploy semantics — does the swap drain in-flight requests and open sockets, or does it SIGKILL the old process?

The realistic options

Fly.io

A strong Rust runtime story and a genuinely useful escape hatch on the build side. `fly launch` recognises a Cargo project and will generate a multi-stage Dockerfile for you; more importantly, you can build the image wherever you like — including a machine you control with as much RAM as you want — and push it, which sidesteps the entire question of how big their builder is. That option alone resolves the worst Rust hosting failure mode. The runtime is real VMs, so WebSockets, tokio background tasks, and connection pools all behave normally, and machines can stop and start on demand rather than idling at full price.

The trade-offs are the ones you'd expect from a platform that hands you machines: you are closer to the infrastructure than on a classic PaaS, multi-region introduces write-forwarding and replica-lag decisions a single-region app never faces, and their managed database story has historically leaned toward primitives rather than a fully managed service — check its current state rather than assuming. Best when the service is long-lived and you want control.

Railway

The nicest first hour on this list. Connect a repo, get a service, add Postgres from a menu, and let variables reference each other across services so `DATABASE_URL` simply appears. Their build system handles Cargo projects without a Dockerfile in the common case, and per-branch environments make preview deploys pleasant in a way that matters more for Rust than for most languages, because a preview environment you can't afford to rebuild is a preview environment you stop using.

What to verify before committing a large workspace: the resources available to the builder, and exactly what persists between builds. A small Axum service will be fine. A workspace with a hundred dependencies and fat LTO is where you find the ceiling, and you want to find it during evaluation rather than during an incident. Usage-based pricing also rewards knowing what your app actually consumes — which, for Rust, is usually pleasantly little at runtime and unpleasantly a lot during the build.

Render

Boring in the best sense: a Heroku-shaped container platform where a Rust service is a long-running web service, background workers and cron are first-class objects, managed Postgres lives next door, and a repo-level blueprint describes the whole thing. You can bring a Dockerfile or give it a build command and a start command — `cargo build --release` and `./target/release/server` is the whole configuration for most apps. WebSockets work, the process stays alive, and deploy semantics are predictable.

The thing to check is the same as everywhere: the build instance's resources and whether the cache persists across deploys. If you're running a single Rust API with a database and you want to stop thinking about hosting, this is a good default and I'd recommend it over anything more exotic.

Shuttle

The Rust-native option, and the only one on this list designed by people who assume Cargo is the centre of the universe. You annotate your entrypoint, declare infrastructure — a Postgres database, a secret store — as arguments to your main function, and deploy from the CLI. Provisioning becomes part of the type system rather than a separate YAML dialect, which is a genuinely different idea and not just a nicer wrapper. For a solo developer shipping an Axum service, or a small team that wants zero infrastructure concepts between `cargo` and a URL, it is the most pleasant thing here and I'd say so plainly.

The trade-offs come from the same design. Your application is coupled to the framework's model of the world, which is delightful while you're inside it and awkward when you need something it doesn't express. The escape hatch to "just run my binary somewhere else" is narrower than with a plain container. And it's a specialist vendor: a smaller surrounding ecosystem, fewer adjacent managed services, and the usual concentration risk of depending on a niche company for something load-bearing. Verify the current build resources and supported project shapes directly, especially for large workspaces.

Koyeb

Git-driven deploys with either a buildpack or your own Dockerfile, global placement, and scale-to-zero available as a normal setting rather than a workaround. For a Rust service — small binary, small idle footprint, fast to start — scale-to-zero is an unusually good fit, because the language gives you nothing to warm up. Worth checking: the native Rust build path versus bringing your own image, the builder's resources, and how their proxy handles long-lived connections if you're running WebSockets.

Google Cloud Run

If you're already on Google Cloud, this is a strong answer for a specific reason that has nothing to do with the runtime: the build side is a separate, configurable service, and being able to pick a build machine with plenty of memory is exactly the knob a heavy Rust build needs. You own the container image (or use buildpacks), which for Rust is less of an imposition than it sounds, since you probably wanted a multi-stage Dockerfile anyway.

The runtime caveats are the well-known ones and all of them are configurable rather than fatal: maximum request duration also bounds how long a WebSocket may live; CPU may or may not be allocated outside request handling, which decides whether your background tokio tasks run at all; and instances are autoscaled and individually unaddressable, which matters if your architecture assumed otherwise. Read the current limits yourself — this is precisely the area where a vendor's constraints change between writing and reading.

AWS Lambda with cargo-lambda

Credit where it's due: Rust is arguably the best language Lambda has. There is no runtime to initialise, so the cold start is close to the platform's own floor; the deployment artifact is small; `cargo-lambda` handles cross-compilation to the Lambda execution environment, including arm64, without you learning anything about container images. For request-scoped, spiky, event-driven work — an S3 trigger, a queue consumer, an API endpoint with lumpy traffic — this is genuinely excellent and often the cheapest correct answer.

It is also the sharpest mismatch on this list for a typical Rust web service. The execution environment is frozen between invocations, so `tokio::spawn` loops, in-memory caches, and anything periodic stop existing in any meaningful sense. Each warm environment holds its own database connections, so a burst of concurrency becomes a burst of Postgres connections and you need a pooler in front. WebSockets require a different architecture entirely rather than a configuration change. Pick Lambda when the work is genuinely request-scoped; don't pick it because Rust is fast on it.

Northflank

The option for teams that want a real platform with knobs exposed. Build pipelines are first-class with configurable build resources — which is the single most relevant feature on this page for anyone whose release build keeps dying — alongside services, cron jobs, managed Postgres and Redis, and bring-your-own-cloud if compliance requires it. If your Rust build has outgrown the "pick a plan and hope" model, being able to specify what the builder gets is worth more than any runtime feature.

The trade-off is surface area: it's more platform than a side project needs, and there's a corresponding amount to learn. For a team running several services with real build requirements, that's a feature. For one API and a database, it's overhead.

A VPS with systemd

Still an excellent, unfashionable answer for Rust specifically. The artifact is one binary; deployment is rsync plus a restart; systemd handles supervision, restarts, log capture, and graceful shutdown better than most people give it credit for. There's no image registry, no buildpack, and nothing to detect incorrectly.

Two honest caveats. First, build in CI and ship the binary — building on a small VPS is precisely the OOM story this post opened with, and doing it on the machine that's currently serving traffic compounds the mistake. Second, you own TLS, patching, backups, and the pager. For one service, that's a fair trade and the economics are unbeatable. For twenty, it stops being one.

# /etc/systemd/system/checkout-api.service
[Unit]
Description=checkout-api (axum)
After=network-online.target
Wants=network-online.target

[Service]
Type=exec
User=app
Group=app
WorkingDirectory=/srv/checkout-api
Environment=PORT=8080
Environment=RUST_LOG=info,tower_http=debug
# DATABASE_URL and friends. chmod 600, owned by root, not in the repo.
EnvironmentFile=/etc/checkout-api.env
ExecStart=/srv/checkout-api/bin/server
Restart=on-failure
RestartSec=2

# Graceful shutdown. axum's with_graceful_shutdown() waits on SIGTERM; give
# in-flight requests and open WebSockets time to finish before the hammer.
KillSignal=SIGTERM
TimeoutStopSec=30

# A Rust binary needs almost nothing from the host, so take almost
# everything away. This costs one minute and removes a lot of blast radius.
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/checkout-api
CapabilityBoundingSet=
AmbientCapabilities=

[Install]
WantedBy=multi-user.target

PandaStack

Mine, so apply the appropriate discount. PandaStack is git-driven app hosting on Firecracker microVMs: connect a repo, push to deploy, and each deploy builds into a fresh microVM that takes traffic once health checks pass. There's no Dockerfile — a `Cargo.toml` at the root is enough to detect the project, and you can override the build and start commands when the repo has several binaries. Each app gets its own kernel and real per-app CPU and memory rather than a slice of a shared-kernel container, so a long-lived Axum service with open WebSockets, `tokio::spawn` background loops, and a live connection pool behaves exactly the way it does on your laptop. Managed Postgres 16 is a create-and-attach environment variable away, provisioned in 30–90 seconds.

The substrate is the part that's actually different. Machines boot by restoring a snapshot rather than booting cold — about 179ms at p50 and 203ms at p99 end to end, with a roughly 49ms restore step, against about 3s for a genuine first cold boot of a template. That makes scale-to-zero honest rather than aspirational, which suits Rust unusually well: a Rust binary has nothing to warm up, so if the platform's own wake latency is small, an idle service can genuinely be nothing at all and still answer the first request without anyone noticing. Forking a running machine takes 400–750ms on the same host, or 1.2–3.5s across hosts, which is what makes an environment per branch cheap enough to actually use.

It's also a good place to put builds you don't trust, which for Rust is a real category — `build.rs` scripts and procedural macros execute arbitrary code on your builder at compile time, with whatever credentials that machine has. Because each agent pre-allocates 16,384 /30 subnets, every build VM gets its own network namespace, so registry-only egress (or none at all, with vendored dependencies) is the default rather than a special case. That's covered in more depth at /blog/sandbox-untrusted-rust-cargo-build.

from pandastack import Sandbox

def build_and_test_crate(repo_tar: bytes, commit: str) -> dict:
    """Compile and test a Rust crate inside a throwaway microVM.

    The build gets its own kernel, its own network namespace, and a hard TTL,
    so a runaway build.rs, a target/ directory that grows without limit, and
    an OOM all stay inside a machine that was going to be deleted anyway.
    """
    # `with Sandbox.create(...) as sbx:` does the same thing; the explicit
    # form is used here so the failure path is visible.
    sbx = Sandbox.create(template="base", ttl_seconds=1800)
    try:
        sbx.filesystem.write("/work/build.sh", "set -euo pipefail\ncd /work/src\n")
        sbx.exec("mkdir -p /work/src", timeout_seconds=30)
        sbx.filesystem.write("/work/repo.tar", repo_tar)
        sbx.exec("tar -xf /work/repo.tar -C /work/src", timeout_seconds=120)

        # -j 2 is the cheapest defence against the OOM killer: it bounds how
        # many rustc/LLVM processes exist at once. --locked keeps the build
        # honest about which dependency versions you actually reviewed.
        build = sbx.exec(
            "cd /work/src && cargo build --release --locked -j 2 2>&1",
            timeout_seconds=1500,
        )
        if build.exit_code != 0:
            return {"commit": commit, "ok": False, "stage": "build",
                    "logs": build.stdout[-8000:]}

        test = sbx.exec(
            "cd /work/src && cargo test --release --locked 2>&1",
            timeout_seconds=900,
        )

        binary = None
        if test.exit_code == 0:
            # Pull out the one thing worth keeping. Returns bytes.
            binary = sbx.filesystem.read("/work/src/target/release/server")

        return {"commit": commit, "ok": test.exit_code == 0, "stage": "test",
                "logs": test.stdout[-8000:], "stderr": test.stderr[-2000:],
                "binary": binary}
    finally:
        # target/, every object file, and anything build.rs decided to write
        # die with the machine.
        sbx.kill()

The honest caveats. PandaStack does not repeal LLVM: a fat-LTO release build of a large workspace needs a build environment with enough memory, and if you give it a small one it will fail the same way it fails everywhere else. Size the build environment deliberately, or build in CI and deploy the artifact — which is a perfectly respectable answer on any platform on this page. Scale-to-zero and long-lived WebSockets are also in tension by definition, since a service holding open sockets is never idle, so the idle economics only pay off for workloads that genuinely go quiet. And it's younger than most things above it, with a smaller ecosystem. Worth it if per-app kernel isolation, real scale-to-zero, or cheap per-branch environments are things you actually need; skip it if Render or Railway already fits.

The one-line version

  • Fly.io — Build story: bring your own image, including one built on a machine you control, which removes the builder-size problem entirely. Runtime story: real VMs, WebSockets and background tasks are ordinary, machines can stop when idle. Best for: long-lived Rust services where you want control and don't mind being close to the infrastructure.
  • Railway — Build story: detects Cargo without a Dockerfile; verify builder resources and what persists between deploys before committing a large workspace. Runtime story: long-running services, one-click Postgres, per-branch environments. Best for: fastest path from git to a running Axum app.
  • Render — Build story: build command plus start command, or your own Dockerfile; check builder size and cache persistence. Runtime story: long-running web services, first-class workers and cron, managed Postgres next door. Best for: one Rust API and a database, with no drama.
  • Shuttle — Build story: Rust-native, Cargo-centric, infrastructure declared in your code rather than in YAML. Runtime story: managed for you, with a framework model you adopt wholesale. Best for: solo developers and small teams who want zero infrastructure concepts between cargo and a URL.
  • Koyeb — Build story: buildpack or Dockerfile, git-driven. Runtime story: global placement with scale-to-zero as a normal setting, which suits a small Rust binary well. Best for: a light service that should cost nothing while nobody is using it.
  • Google Cloud Run — Build story: the strongest knob on this list, because the build service lets you pick a machine with real memory. Runtime story: containers with request-oriented scaling — check max request duration and CPU-outside-requests. Best for: heavy builds inside an existing GCP footprint.
  • AWS Lambda + cargo-lambda — Build story: cargo-lambda cross-compiles to the Lambda environment with no container knowledge required. Runtime story: frozen between invocations, so no background tasks, no in-memory cache, and pooling needs help. Best for: genuinely request-scoped and event-driven work, where Rust is close to the ideal runtime.
  • Northflank — Build story: configurable build resources as a first-class pipeline feature — the direct answer to a build that keeps dying. Runtime story: services, jobs, managed Postgres and Redis, BYOC. Best for: teams whose Rust builds have outgrown fixed-size builders.
  • VPS + systemd — Build story: not the VPS's job — build in CI and rsync the binary. Runtime story: one binary under systemd, with graceful shutdown and cheap hardening. Best for: one or two services, best economics, if you're willing to own patching and TLS.
  • PandaStack — Build story: git-driven, no Dockerfile, Cargo detected from the repo — but a heavy release build still needs a build environment sized for it. Runtime story: Firecracker microVM per app with its own kernel, real CPU/RAM, managed Postgres, and snapshot-restore that makes scale-to-zero honest. Best for: per-app isolation, idle-costs-nothing staging, and cheap environment-per-branch.

Pick by situation

  • One Axum or Actix API with a Postgres database, and you'd like to stop thinking about it → Render, or Railway if you value the first hour more than the fifth month.
  • You're a Rust developer who wants the least infrastructure between `cargo` and a URL → Shuttle.
  • Your release build keeps getting OOM-killed and you're tired of it → Northflank or Cloud Run for configurable build machines, or Fly.io and build the image yourself on hardware you choose.
  • The service is event-driven and genuinely request-scoped → Lambda with cargo-lambda. Rust is close to the perfect fit there and it would be silly to run a machine for it.
  • The service holds thousands of WebSockets → anything with real long-running processes: Fly.io, Render, Northflank, a VPS. Not functions, and check the proxy's connection-duration limit before you commit.
  • You want staging and per-branch environments that cost nothing while nobody's looking → Koyeb or PandaStack, mine, for the scale-to-zero economics.
  • You're building something that compiles code it didn't write — a CI service, a coding agent, a plugin host → a microVM boundary rather than a container one, because build.rs runs arbitrary code at compile time.
  • You have one service and someone who enjoys owning a server → a VPS and a systemd unit. It's cheap, it's boring, and it will still be running in three years.

The short version

Evaluate Rust hosts on the build first, because that's the half that actually differs. Ask how much memory the builder has, whether you can make it bigger, and what survives between deploys. Then fix the things on your side that no platform can fix for you: thin LTO instead of fat, a sane `-j`, `cargo-chef` so a one-line change doesn't recompile the world, and `sccache` if your builders are ephemeral.

The runtime half is comparatively easy, and the only real question is whether the platform lets a process stay alive. If your service holds sockets or does periodic work, pick something long-running and stop reading the serverless benchmarks. If it's genuinely request-scoped, Rust on functions is excellent and you should take the free lunch. Get the build right and Fly, Railway, Render, Shuttle, Koyeb, Cloud Run, Northflank, a VPS, and PandaStack will all serve your binary perfectly well — the binary was never the problem.

Frequently asked questions

Why does my Rust build get OOM-killed on CI or on my hosting platform?

Almost always because of how release builds spend memory. Cargo compiles your whole dependency graph from source, running many rustc processes in parallel, each with its own LLVM instance and working set — and if you enabled fat link-time optimisation, the final step merges everything into one enormous LLVM module and optimises it, which is the peak. That peak arrives at the END of the build, after all the cheap parallel work has finished, which is why it dies at minute eight rather than failing quickly. The fixes in order of cost: cap parallelism with -j, switch lto from "fat" to "thin", lower codegen-units, drop debug info from the release profile, and only then pay for a bigger builder. Check disk too — a large target/ directory fills small ephemeral volumes and produces a different error with the same vibe.

Do I need a Dockerfile to deploy a Rust app?

No, though more platforms expect one for Rust than for Node or Python. Several hosts detect a Cargo project and will run cargo build --release and start the resulting binary from build and start commands you configure, with no image to maintain. A Dockerfile becomes worth writing when you want precise control over the build stages — which for Rust is a real benefit, since a multi-stage build with cargo-chef is the difference between rebuilding your dependencies on every commit and rebuilding them only when a manifest changes. It also pins the runtime base image, which matters if a dependency links against system OpenSSL or another shared library. If you go without one, be explicit about which binary to build when the workspace has several.

How do I make Rust deploys faster?

Stop rebuilding dependencies. The single biggest win is cargo-chef: it computes a dependency-only recipe from your manifests and builds it as its own layer, so editing your source no longer invalidates the expensive part. Layer BuildKit cache mounts on top for the crates.io registry and target directory, and add sccache if your builds run on ephemeral machines that never see the same disk twice — sccache can be backed by object storage, so a fresh builder still gets warm hits. Then verify it worked: run two consecutive deploys with no source change and time them. If the second isn't dramatically faster, the platform isn't persisting the cache no matter what your Dockerfile says. Finally, use --locked so the build never silently resolves to different versions than you tested.

Is AWS Lambda a good fit for a Rust web service?

It's a superb fit for request-scoped work and a poor fit for a typical long-running service, and the distinction matters more than the language. Rust on Lambda is close to ideal on the performance axis: no runtime to initialise, a tiny artifact, and cargo-lambda handles cross-compilation to the execution environment including arm64 without you learning container tooling. The problem is structural. The environment is frozen between invocations, so tokio::spawn background loops, periodic timers, and in-memory caches stop functioning as designed; each warm environment holds its own database connections, so concurrency bursts become connection bursts unless you put a pooler in front; and WebSockets require a different architecture rather than a setting. Use it for queue consumers, event handlers, and spiky APIs. Use a long-running process for anything that does work between requests.

Should I build a static musl binary for deployment?

It's a genuine convenience and not a requirement. A fully static musl build runs on essentially any Linux — including a scratch container — and removes the class of failure where a binary built on a newer distribution refuses to start against an older glibc. The costs are real though: musl's allocator has historically been slower than glibc's under heavily multi-threaded allocation-happy workloads, which describes a lot of async Rust, and some crates with C dependencies need extra work to cross-compile. The pragmatic middle ground most teams land on is a glibc build in a multi-stage Dockerfile whose runtime stage uses the same distribution family as the builder, plus ca-certificates and whatever shared libraries your dependencies actually link. Measure before adopting musl for performance reasons; adopt it for portability reasons with your eyes open.

Keep reading

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.