drift Docs
Start
What is Drift?
The tour, if you are new here.
Use cases
Whether Drift does your thing.
Getting started
Nothing to deployed, in one command.
Architecture
How a slice is put together.
What it costs
The free grant, four unit prices, two rules.
Build
Canvas
Static sites, same origin as your API.
Tools
Operate
Auth
Accounts, tokens and scopes.
Security
Boundaries, sandboxing and hardening.
Troubleshooting
Error codes
What went wrong, and what to do about it.
Legal
Acceptable use
What a slice may not be used for.
Data processing
The DPA, and every sub-processor.

Memory and concurrency

Every function books memory in the slice's shape, from 8MB to 256MB, and there is no default. That one number does three jobs: it is what the function is billed at, it is the memory its invocations may use, and it is what decides how many of them run at once. Most of this page is about the third, because it is the one that surprises people.

A compiled function floors at 16MB, not 8MB.

Go and Rust carry a private language runtime inside every invocation — the Go runtime alone holds about 8.6 MB before your code runs — where Python, Node.js, Ruby and PHP share one language server per language and genuinely fit in 8 MB. A compiled function booked below 16MB is refused at deploy rather than served badly, because the reservation clamps to the pool instead of failing cleanly: single requests would return 200 and the slice would only give way under concurrency. See what it costs for what each floor prices at.

The booking is a pool, not a per-call cap

It would be reasonable to read a booking of 64 MB as "each call may use 64 MB". It does not mean that. The booking is a pool the function's concurrent calls share: while calls are in flight, what they hold together stays inside it.

concurrency = what you booked / what one call actually costs

So a function measured at 8 MB per call runs eight-up inside a 64 MB booking, and a function measured at 30 MB runs two-up inside the same booking. Booking more buys both headroom per call and more calls at once, from one dial. Nothing else needs configuring, and there is no separate concurrency setting to keep in step with it.

This is why the cheapest tier is a real choice and not a trap.

An interpreted function that reads and writes Backbone and returns costs very little per call, so 8 MB buys it genuine parallelism for 24 cents a month. A function that loads a document into memory to render it costs a lot per call, and the same 8 MB would serve one caller at a time. Book for the work, not for the reassurance.

When the pool is full

A new call that will not fit is refused with 503 and Retry-After: 1. It clears on its own as calls finish, and the response carries X-Drift-Reason: FUNCTION-MEMORY so your own logging can tell it apart from anything else that answers 503.

It is not a 429, and the difference is the point. A 429 means the caller sent more than they were allowed and the remedy is theirs. This is the opposite: the caller did nothing wrong, and the remedy is yours — book more memory, which is also more concurrency. Those two point at different knobs, so they carry different codes.

Work is refused at the door. A call that is admitted has room; the platform does not accept work it has no space for and discover the problem halfway through.

When one call outgrows the booking

Estimates are estimates, so occasionally a call that was admitted grows past what its function booked. That single invocation is stopped and its caller gets 500 with X-Drift-Reason: EVICTED. Everything running beside it keeps going — the other calls to that function, every other function, and the rest of the slice.

That containment is the entire reason it works this way. The alternative is letting one call keep growing until something larger runs out of memory, at which point everything in flight dies together and one bad request becomes an outage. One failed request is a much better outcome, and it is the one you get.

An eviction usually means the booking is too small, not that the code is wrong.

If a function's calls all grow together past their booking, it was sized under what it needs. The remedy is a bigger memory, and your function's log says so at the moment it happens — naming the function and the figure. It carries no Retry-After, because retrying the same request against the same booking fails the same way.

Your process may be reused

Starting a process is not free: sandboxing it and initialising its language runtime costs about 2.5 milliseconds of CPU before a line of your code runs. Paying that on every request is most of what a compiled function costs, so Drift keeps the process alive and sends it the next call.

So a function can see state it left behind. A package-level variable, a cached client, a connection you opened last call — those survive into the next invocation of the same function. That is usually what you want, and it is why a database handle is worth keeping in a global. It also means anything you leave in a global is still there next time, so do not use one to hold a single request's data.

The bound is fixed, and it is part of the contract rather than an implementation detail:

  • A process serves at most 200 invocations, then it is retired and the next call gets a fresh one.
  • A process idle for 60 seconds is retired, so a function that stops being called stops holding memory.
  • It is retired immediately when you deploy, so a call never reaches the previous version of your code.

What is never shared: one function's process is never given to another function, and never to another tenant. Your scratch directory is still created fresh for every single call and removed after it, so os.TempDir() is private to one invocation even though the process is not. Nothing about the sandbox changes — the same restrictions apply on call 200 as on call 1.

Today process reuse applies to Go; Rust, Ruby and PHP still get a fresh process per call. Python and Node are reused more broadly, and have been since they launched: one interpreter serves every function of that language in your slice, so two of your Python functions can see each other's module state. Still only ever your own slice — never another tenant's.

Picking the number

Guessing high wastes money and guessing low means calls are refused under load, and neither is discoverable by reading your own source. So don't guess — the platform has been measuring it all along:

drift file benchmark prints what every function in your slice has cost and the size it should book — the high-water mark per function, what it currently books, what it should book instead, and the concurrency that booking is buying you today. Add --apply and it opens the slice's shape with each measured recommendation already filled in, so the number that gets bought is the one that was measured. Nothing is bought until you submit the form.

It also reports how many calls each figure is based on, which is the number to look at first. A recommendation backed by three invocations of one code path is not the same claim as one backed by thousands, and a function nobody has called yet says so rather than reporting a confidently small number.

Any whole number of MB in range is legal. If you measured 20 MB, book 20 — there is no grid to round up to, because rounding up would bill you for memory you never asked for.

One more 503

A slice under overall memory pressure sheds new work briefly with 503 and Retry-After: 5, carrying X-Drift-Reason: SLICE-MEMORY. It clears by itself and needs nothing from you; it is listed here so that your logs can tell it apart from a function that has filled its own pool, which does need something from you.

Every code and header on this page is collected in one table on Limits and alerts, alongside timeouts and rate limits.