The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the GenHTTP Lambda listing page.
Write a C# snippet that returns a GenHTTP IHandler, press deploy, and get a
public URL that serves it. The snippet is compiled with Roslyn at runtime and
the resulting handler is hosted by the same GenHTTP server that
serves this application.
The application is then available at http://localhost:8080/. Its data - the
SQLite database, the stored code and the lambda workspaces - lives in a named
volume mounted at /data.
Two processes: the .NET server, and Vite serving the frontend with hot reload.
Work against http://localhost:5173/ while developing. The server on its own
answers with a placeholder page until a frontend has been built into its web
root, which is what npm run build does:
Build and test everything the way CI does:
The tests compile and execute real lambdas against a real server on a random port, each against its own temporary data directory.
| Route | Serves |
|---|---|
/ | the landing page |
/editor/create | the creation assistant |
/editor/:privateKey | the editor for one lambda |
/lambda/:publicKey | the deployed handler |
| any path, at a lambda's own domain | the deployed handler of a premium lambda with that domain |
/api/v1/ | everything the editor calls, see below |
/mcp | the same, for agents |
/admin | every lambda on the server, for whoever runs it |
Browsable at /api/v1/scalar/. A lambda is addressed by its editor key, which
is also what authorizes the call. Things that can be listed, read, created or
changed are resources; what is a process rather than a thing is a verb in the
path.
| Endpoint | Does |
|---|---|
POST /lambdas | creates a lambda |
GET / PATCH / DELETE /lambdas/:privateKey | reads, changes (its key), removes it |
GET /lambdas/:privateKey/export | the lambda as a runnable project (zip) |
GET / POST /lambdas/:privateKey/versions | lists versions, saves a new one (optionally with specification and change) |
GET /lambdas/:privateKey/versions/:version | reads one version |
GET /lambdas/:privateKey/versions/:version/zip | one version's files as a zip |
POST /lambdas/:privateKey/versions/zip | saves a zip of all files as a new version |
POST /lambdas/:privateKey/versions/changes | changes some files of the newest version |
GET /lambdas/:privateKey/deployment | what is online, and until when |
POST /lambdas/:privateKey/deployment/start / stop | puts a version online, takes it off |
GET /lambdas/:privateKey/deployment/history | every stretch it was online, and what ended it |
GET /lambdas/:privateKey/summary | the dashboard: state, traffic, problems, storage |
GET /lambdas/:privateKey/traffic | requests by minute and quarter hour, statuses, paths |
GET /lambdas/:privateKey/logs | its own log, followed with ?since=; no visitor addresses |
GET /lambdas/:privateKey/agent | the agent's change of it - under way, or the last one - and how many are left today |
POST /lambdas/:privateKey/agent/start / stop | asks the agent for a change, stops it |
GET /lambdas/:privateKey/files | lists the workspace |
GET / PUT / DELETE /lambdas/:privateKey/files/:path | one file, its path encoded (a%2Fb.txt), as base64 up to 32 MB |
GET / PUT /lambdas/:privateKey/files/:path/content | one file as it is, streamed, however large |
PUT /lambdas/:privateKey/folders/:path | makes a folder |
POST /lambdas/:privateKey/code/check | compiles without saving |
POST /lambdas/:privateKey/code/semantics, completions, definition | what the editor asks the compiler |
GET /keys/:publicKey | whether a key is free, and if not, online |
GET /demos | the demos, and the keys to read them with |
POST /builds, GET /builds/:id | the text box on /build |
GET /system | terms, limits, starters, build agent |
GET /telemetry, /logs, /admin/... | for whoever runs the installation |
Creating a lambda asks what somebody would like to build and offers the demos
in those words - "Keep track of things", "Let people sign up" - next to an empty
lambda. Picking a demo starts the new lambda as a copy of it, which is theirs to
change. The files are in Resources/Templates and listed in TemplateCatalog.
A lambda has two keys. The public one is part of its URL and may be changed; the private one is the editor link and is shown only to whoever created the lambda - anyone holding it can edit, deploy and delete.
A lambda may also return a websocket rather than a document, in any of the
three flavours the module offers - Websocket.Functional(), .Reactive() and
.Imperative(). The handshake is an ordinary request and is bounded by the
execution timeout like any other; everything after it belongs to the
connection, which is why a socket may stay open far longer than a lambda is
given to answer. FrameType, which the imperative flavour reads off every
frame, is one of the names a lambda is given, so no import is needed.
The editor says when both free tier timers run out: a hint under the public URL for the deployment, and a chip beside the buttons for the lambda itself, which opens an explanation of how each one is extended.
Editing and deploying are separate: saving creates a version, deploying picks one (the latest by default) and makes it live. A lambda has at most one deployment at a time, and older versions stay available to deploy again.
Where the installation runs the build agent, the editor has a Change
section: the owner says what should be different, and the same agent that
builds things on /build changes the lambda - it reads the code and the
history, changes only what was asked with change_code, fixes what does not
compile, and puts the result online as a new version. Switched off, it saves
the version for the owner to look at and deploy themselves.
The section shows it happen: what the agent says it is doing, each tool it calls with what came of it (the version saved, errors from the compiler, the version online), and a clock against its time limit. Afterwards it offers the difference to the version before, the address, and putting the previous version back. The change is followed by the frame of the editor rather than the section, so the sidebar marks it and the lambda is read again when it ends wherever the owner is; and it is kept by the agent under the lambda, so a reload, a second tab or a redeploy of the server finds it where it got to.
It shares the queue and the daily allowance of /build, one change of a
lambda runs at a time, and it can be stopped - whatever it saved stays a
version. A change runs without create_lambda, and its editor key travels in
the brief inside the build container, never in a log line.
A single .NET 11 project hosted by GenHTTP.Full.Ioxide, wired up in
Application.cs and divided into
services that the API resources talk to through interfaces:
Services/Meta) - lambdas and their versions, the only component
that speaks to the database. Its public surface is DTOs, mapped by hand.Services/Storage) - the code itself, on the file system, one
file per version. Never in the database.Services/Workspace) - the private directory of a lambda,
reached from the editor. The same directory the generated Workspace class
writes to from inside a lambda, under the same limits, so a file put there by
hand behaves like one the lambda wrote itself. Only the room it takes is
limited - 256 MB for most lambdas and 2 GB for a premium one, in any number
of files of any size - counted in blocks of 4 KB as the disk counts it, so
an empty file or a folder is not free. The quota is compiled into the
lambda, so moving it to another tier builds it again on its next request.Services/Deployment) - wraps a snippet in a method body,
compiles it with Roslyn, loads the assembly and calls PrepareAsync() on
the resulting handler. Compiled once, then cached.Services/Execution) - an IHandler, not a web service: it
looks up the handler for a request and runs it.Services/Protection) - concerns in front of execution that
resolve the lambda, rate limit per client and cap concurrency.Services/Background) - a small scheduler that undeploys
free tier lambdas a day after their last deployment and removes them after
30 days without a save.The editor asks the compiler what the code means rather than guessing from how
it is spelled. Services/Deployment/Compilation/SemanticClassifier.cs says
what each name is, so a type, a method, a parameter and a local are coloured
apart, and CompletionResolver.cs answers what may be written where the caret
sits - including what follows a dot, which needs the type of what precedes it
and therefore needs a compiler. Both bind without emitting; the browser's own
grammar colours each keystroke in the meantime so nothing waits on the network.
Snippets are compiled against the GenHTTP module surface with its namespaces
already imported, so no using directives are needed. A guard
(Compilation/CodeGuard.cs) rejects code that reaches for the file system,
processes, reflection, sockets or the internals of this application before it
is ever compiled, and a lambda that does need files gets a directory of its
own under /data/workspaces.
Compiled lambdas run in the server process. The guard raises the cost of misbehaving; it is not a sandbox, which is why the container runs unprivileged and the seccomp profile is left alone.
Everything is read from the environment on startup, see
LambdaOptions.
| Variable | Default | Meaning |
|---|---|---|
LAMBDA_PORT | 8080 | port of the root server |
LAMBDA_ENGINE | ioxide | ioxide or kestrel (see below) |
LAMBDA_RECEIVE_QUEUE_ENTRIES | 4096 | reads of 32 KB a connection may have waiting on io_uring |
LAMBDA_DATA_DIRECTORY | ./data | database, code and workspaces |
LAMBDA_WEB_ROOT | ./wwwroot | the built frontend |
LAMBDA_DEVELOPMENT | false | verbose error pages and debug logging |
LAMBDA_DEPLOYMENT_LIFETIME_HOURS | 720 | unused for this long and it goes offline |
LAMBDA_RETENTION_HOURS | 2160 | unused for this long and it is removed |
LAMBDA_MAINTENANCE_INTERVAL_HOURS | 0.25 | how often expired lambdas are looked for |
LAMBDA_MAX_CODE_LENGTH | 1048576 | characters of C# a lambda may have, in all |
LAMBDA_PREMIUM_MAX_CODE_LENGTH | 10485760 | the same for a premium lambda, never less |
LAMBDA_MAX_ASSET_BYTES | 33554432 | what the shipped assets may come to |
LAMBDA_PREMIUM_MAX_ASSET_BYTES | 134217728 | the same for a premium lambda, never less |
LAMBDA_WORKSPACE_BYTES | 268435456 | the room a workspace may take in all |
LAMBDA_PREMIUM_WORKSPACE_BYTES | 2147483648 | the same for a premium lambda |
LAMBDA_MAX_VERSIONS | 50 | versions kept per lambda |
LAMBDA_RATE_LIMIT | 5000 | lambda requests per second and client |
LAMBDA_MAX_CONCURRENCY | 64 | lambda requests executed at once |
LAMBDA_EXECUTION_TIMEOUT_SECONDS | 15 | before an invocation is aborted |
LAMBDA_TELEMETRY_INTERVAL_SECONDS | 30 | how often a reading is taken |
LAMBDA_TELEMETRY_SAMPLES | 2880 | how many readings are kept |
LAMBDA_PUBLIC_ACTIVITY | true | serve the per lambda activity to anyone |
LAMBDA_ADMIN_TOKEN | - | enables the panel, and closes the figures |
LAMBDA_LOG_HISTORY | 4000 | log lines the panel can read back, to 10^6 |
LAMBDA_LOG_MEMORY_MB | derived | what their text may cost; whichever runs out first |
LAMBDA_LOG_LAMBDA_OUTPUT | true | keep what lambdas print, filed under them |
LAMBDA_LOG_REPEAT_WINDOW_SECONDS | 10 | fold identical lines; zero writes every one |
LAMBDA_LOG_CLIENT_ADDRESS | true | record the address a request came from |
LAMBDA_LOG_GEO | true | say which country a range is registered in |
LAMBDA_LOG_GEO_REFRESH_HOURS | 168 | how often the registry files are refetched |
LAMBDA_LOG_GEO_PLACES | true | also fetch the town and network databases |
LAMBDA_LOG_MAX_LINES_PER_REQUEST | 200 | before one request's output is cut off |
LAMBDA_MCP_ORIGINS | - | hosts a browser may use /mcp from |
LAMBDA_PUBLIC_URL | - | canonical address, enables the sitemap |
LAMBDA_TLS_PORT | 0 | port for TLS, zero leaves it off |
LAMBDA_CERTIFICATE | - | PEM chain or PKCS#12 archive |
LAMBDA_CERTIFICATE_KEY | - | private key, for a PEM pair |
LAMBDA_CERTIFICATE_PASSWORD | - | password of the PKCS#12 archive |
LAMBDA_CERTIFICATE_DIRECTORY | - | further certificates, one folder per name |
LAMBDA_ACME_DIRECTORY | - | web root an ACME client writes challenges to |
The io_uring engine is the default and the faster one, but container runtimes
block the syscall in their default seccomp profile - so the image defaults to
LAMBDA_ENGINE=kestrel. Set it back to ioxide where io_uring is available
and allowed.
That override runs the server on io_uring under seccomp-ioxide.json, which is
the default profile of the runtime with io_uring_setup, io_uring_enter and
io_uring_register added and nothing else. Without it the engine stops at
io_uring_setup failed: -1 before the first request. The hole is small but it
is real: io_uring reaches further into the kernel than ordinary sockets, and
this platform runs code written by strangers in the same process.
The engine does not slow a sender down while a request is read more slowly
than it arrives. It keeps what came in, and once LAMBDA_RECEIVE_QUEUE_ENTRIES
reads are waiting it drops the connection with recv queue overflow on stderr.
Its own default of 64 is two megabytes, which a JSON body of a few megabytes
outruns while it is being parsed - an upload of 24 MB was dropped even at
5 MB/s. The reads come out of the 4096 each reactor shares, and when those run
out it waits for them rather than dropping anybody - so the default here is
4096, at which a connection is only ever slowed down: 200 MB went through at
the full speed of the loopback, where 1024 dropped 100 MB sent at 50 MB/s. The
price is that one large upload can hold the reads of its reactor while it is
consumed, and the other connections on that reactor wait for them. Kestrel
slows the sender down and needs none of this.
A workspace file sent as it is, to …/files/:path/content, is written to the
disk as it arrives: 200 MB went through with the server growing by 160 MB,
where base64 in JSON costs several times the file. A connection dropped in the
middle of a body looks like a body that ended, so such an upload is checked
against its Content-Length and not kept unless all of it arrived - with 1024
reads, the partial file had been kept as if it were the whole.
An installation that also runs the build agent is spread over three compose
files, and leaving one out does not fail loudly - it produces a server that
looks healthy while some part of it is switched off. Drop the agent overlay and
/build reports "no build agent"; drop the ioxide overlay and the engine
quietly falls back to Kestrel. So there is a script that knows the list and
checks the result:
--check is safe at any time and is the quickest way to answer "is the build
agent actually up?". A deploy with nothing to change recreates nothing, so it
costs no downtime; when there is something to change it restarts the container,
which drops every open connection - including anyone using a hosted lambda at
that moment.
Linking it onto the path makes it available to anyone with sudo:
/stats shows what the process is doing, and /api/v1/telemetry is where it
comes from. A reading is taken every LAMBDA_TELEMETRY_INTERVAL_SECONDS and
LAMBDA_TELEMETRY_SAMPLES of them are kept, which is a day at the defaults.
The readings live in memory and go with the process. That suits what they are for - a restart ends the run they were measuring - but it does mean a redeploy starts the graph over.
It also lists what each lambda has served, busiest first. Only the public key
identifies one there - the editor key, the code and the visitors are not part
of it - and LAMBDA_PUBLIC_ACTIVITY=false stops that list being served without
stopping anything being counted.
The page is otherwise aggregates only: no key, no code and no client address leaves through it, which is what makes it safe to serve to anyone. What it is for is the shape of the memory curve. A managed heap that climbs across gen 2 collections is a leak; committed bytes climbing while the managed heap stays flat is the heap keeping pages it could return, which is not.
The server terminates TLS itself, so nothing has to sit in front of it. Point
LAMBDA_CERTIFICATE at a certificate and set LAMBDA_TLS_PORT, and the plain
port starts answering with a redirect to the secure one.
A PKCS#12 archive works just as well - leave the key empty and set
LAMBDA_CERTIFICATE_PASSWORD instead. The certificate is read again when the
files change, so a renewal is picked up without a restart.
A lambda is not only C#. Any file whose name does not end in .cs is an asset:
it is served as it is, never compiled, and costs none of the code budget.
The snippet reaches them through Assets, which is the other half of
Workspace - what the lambda shipped, against what it has written since:
Assets.App() is a single page application over them: index.html is the
shell and a path matching no file is answered with it. Assets.Tree() and
Assets.Files() are there for anything less opinionated, and content types
come from the extension.
The directory is rewritten from the version being deployed, so an asset dropped
from a version stops being served rather than lingering. LAMBDA_MAX_ASSET_BYTES
is what they may come to in total, and LAMBDA_PREMIUM_MAX_ASSET_BYTES the same
for a lambda in the premium tier. How many there are is not limited, and
neither is the number of C# files - only what the code comes to, by
LAMBDA_MAX_CODE_LENGTH and LAMBDA_PREMIUM_MAX_CODE_LENGTH.
Budget the disk for the premium tier. Assets are kept inside every version, as
base64 in JSON, so each version saved at the limit takes nearly half as much
again on disk - 184 MB for 128 MB of assets - and LAMBDA_MAX_VERSIONS of them
are kept: at the defaults, a premium lambda that is saved over and over with
all 128 MB of its assets can come to around 9 GB of history.
Budget the memory too. A version is read and written whole: with the 128 MB
of assets the premium tier allows, saving one took the server to 2.3 GB and
deploying it to 2.8 GB. Even the 32 MB of a free lambda took a server idling
at 350 MB to 950 MB to save, and anybody can create a free lambda. Set LAMBDA_MAX_ASSET_BYTES and
LAMBDA_PREMIUM_MAX_ASSET_BYTES to what the machine can carry until assets are
stored apart from the versions.
A certificate is only good for the names it carries, so a server answering to
several needs one per name. LAMBDA_CERTIFICATE_DIRECTORY points at a folder
holding the rest, a subdirectory per name in the layout an ACME client already
keeps them in:
The names come from the certificates themselves rather than the folder names, including wildcards, and a client asking for a name none of them covers is answered with the default one.
Note that this needs a PEM pair per name when running on the io_uring engine. Kestrel terminates TLS in .NET and is handed a loaded certificate, but the io_uring engine reads the files itself and has no way to be given an archive.
The issuers in the file are published into the certificate store of the user the server runs as, because a client needs the chain and not just the leaf to reach a root. Kestrel papers over a missing chain by fetching the issuer over the network mid-handshake; the io_uring engine sends what it was given, so a server that never published its issuers answers it with a certificate nobody can verify.
There is no ACME client built in. With certbot, the private key stays readable by root only while the server runs unprivileged, so a deploy hook publishes the renewed files where the container can read them:
Because the server holds port 80, the standalone authenticator needs it back for the few seconds a renewal takes - a pre hook stops the container and a post hook starts it again.
The server can answer the challenges itself instead. Point
LAMBDA_ACME_DIRECTORY at the folder the ACME client writes them into, and
GET /.well-known/acme-challenge/{token} is served from
{folder}/.well-known/acme-challenge/{token} - for every host the server
receives, a lambda's own domain included, where the path would otherwise be
the lambda's. Nothing else in the folder is served.
The challenge is asked for over plain HTTP, and it is the one request the upgrade to HTTPS lets through rather than redirecting.
A lambda in the premium tier can answer at a domain of its own, which its owner sets in the editor after pointing the domain's A and AAAA records at the server. Plain requests to it are redirected to HTTPS on the same domain like every other request, so it needs a certificate - issued by hand for now, with the web root above, into a folder of its own:
The folder is only looked through on startup - the io_uring engine learns the names it holds certificates for when it opens the TLS port - so a new certificate is served after the next restart of the container. Renewals of a certificate the server knows are picked up without one. Until the certificate is there, visitors of the domain are redirected to HTTPS and shown the default certificate, which does not carry their name.
There is a Model Context Protocol endpoint at /mcp, so an agent can build
something here and put it online without a person driving the editor. It is
JSON-RPC over a single path, taking POST; a GET is declined with 405, because
this server answers rather than streams.
The tools are the shape of the job: create_lambda, write_code, check_code,
deploy, read_lambda, read_logs, and list_demos for reading something that
already works. platform_guide is the one to call first -
it says what a snippet has to return, what is imported, what is refused, and the
handful of things that catch people out.
write_code takes an optional specification (what the user wants and why) and change (one
line on what the version does). change_code changes only the files it names,
or a passage within one, and either takes deploy: true or check: true -
the latter compiles what it saved and answers with the diagnostics, without
sending every file again the way check_code needs. A deployment that is
refused names the version it refused and the one that is stillOnline. They are kept with the version and shown next
to its diff in the control center, and read_lambda hands the recent history
back so the next agent can read why before it changes anything. read_logs
lets an agent see how what it deployed is answering, stack traces included.
read_lambda sends every file of a version while together they come to 30,000
characters, and names them with their lengths beyond that; file then fetches
one in full, up to a megabyte, and anything larger is left to the zip of the
version - a hundred megabytes of base64 is nothing an agent can read. It also
says which tier the lambda is in and what that allows, and platform_guide
lays out both tiers. The guide, write_code and upload_file also steer a
large file that is data rather than program - a model, a dataset, media - into
the workspace, where it is kept once instead of in every version, and a
refusal for assets over the limit says the same.
The installation keeps a handful of finished lambdas online in the Demo tier,
each showing one way to build something: demo-crud (a REST API over records in
a JSON file), demo-registration (accounts, login and a members page),
demo-game (a websocket game), demo-files (uploads) and demo-live
(server-sent events). Their editor key is their public key, and it is meant to
be announced: read_lambda, list_files and read_logs work on a demo exactly
as on an agent's own lambda, and the editor at /editor/demo-crud shows it.
Everything that would change a demo - saving, deploying, stopping, moving,
deleting, the workspace, the showcase - is refused by its tier, whichever way
the request comes in. create_lambda with a demo's id as its template starts a
lambda of one's own from a copy. Keys starting with demo- cannot be claimed.
The demos are seeded in the background after startup from the hidden templates
of the same name in Resources/Templates, redeployed when their template
changes, and retired when they leave DemoCatalog. Nobody can move a lambda
into the tier or out of it, the operator included.
Nothing is created until acceptTerms is true, and the editor key that comes
back is the only way into what was made. There is no session and nothing is
remembered between calls: everything a call needs is in its arguments.
LAMBDA_MCP_ORIGINS lists the hosts a browser may drive the endpoint from. An
agent speaking HTTP sends no Origin and is never checked against it; the list
is there because a browser does, and a page on another site could otherwise aim
somebody's browser at this endpoint.
/admin lists every lambda, when it was created, whether it is online, and
what its code says - and takes them offline or removes them. /stats is what
the process is holding and what the engine is carrying. Both are reached
through the Admin menu in the header, which is where the token is entered:
it is kept in session storage, so closing the tab locks it again.
This is the one part that authenticates. Everywhere else the editor link is
the credential and it only ever reaches one lambda; this reads code that belongs
to other people and can take their work away, so it asks for LAMBDA_ADMIN_TOKEN
in an X-Admin-Token header. Until that variable is set there is no panel at
all, and a wrong token is answered exactly like a missing one - an installation
that has no panel and one that is guarding it look the same from outside.
The server figures follow the same token, on the rule that an installation with
an administrator keeps them to them. Where no token is configured there is
nothing to check a request against, so requiring one would only mean nobody
could ever read them - there they stay public, as they were before there was a
panel to put them behind, and LAMBDA_PUBLIC_ACTIVITY still decides the per
lambda figures.
/logs is the tail of this run, live. It holds everything the server logged
and everything a lambda printed while it was serving a request - the two are
told apart because the console is shared but the attribution is not: the
concern that serves a lambda marks the request, the mark travels with it
through every await into the code of the user, and the writer over the console
reads it back to decide whose line it is. Constructing a lambda is marked the
same way, so a print at the top of a snippet is filed under it as well.
Identical lines are folded before they are ever written: the same request from
the same caller answered the same way is counted rather than repeated, and one
line every LAMBDA_LOG_REPEAT_WINDOW_SECONDS says how many there were. What
the window bounds is how stale a count may be, not how many are folded into
it - and a line already read never changes underneath the reader, which is what
lets it work with a cursor that only moves forwards. A run that stops has its
last count written when somebody next reads, because otherwise the tail of a
burst would sit counted and unseen.
Fold repeats in the panel does the same thing to what is already loaded, turning the list into one row per distinct line with a count on it, which is what makes a log worth reading when something is repeating: a scanner knocking on five paths every two seconds is five rows rather than fifteen hundred. Each survivor sits where it last happened rather than where it started, because a log is read from the bottom. The same line from two different callers stays two rows - which of them is doing it is usually the question.
Callers lists everyone the log still holds something about - address, where from, how many requests, how many failed, first and last seen - over the whole ring rather than over the page on screen, because who is out there is a question about the run and not about the last screenful.
The effect is that one lambda can be read on its own, which is what the Log
button beside each row in /admin does, and so can one caller - clicking an
address narrows to it. The find box takes bare words that must all appear,
!word for one that must not, and "a phrase" to keep it together; it matches
the text, the source, the lambda and the caller, so !Requests leaves only
what the server and the lambdas said. A warning the server logs about a
lambda - the one the error handler writes when it throws - is filed under that
lambda too, with the stack trace that was kept from the visitor.
Every line says which protocol it arrived over, which country the address is
registered in, and which address it was being written for. That is what turns a
path being hit five hundred times a minute from a mystery into a question, and
it applies to more than the request line: a lambda's print and an error the
server logged both carry the caller, because the request is marked once at the
outside and everything below reads the mark back. A request that arrived
through a proxy is recorded as the address it claimed and the hop it came
from, since the forwarded header is written by whoever sent it - taking it at
face value would let anyone write any address into this log. Addresses and
user agents are pooled, so a million lines from a few dozen callers is a few
dozen strings rather than a million. It is personal data, it is only ever
served behind the token, and LAMBDA_LOG_CLIENT_ADDRESS=false leaves every
line in place with nothing personal on it.
The country comes from the delegation files the five regional registries publish - the same records that say who was given which block - fetched weekly and cached on the data volume, so a server that restarts while they are unreachable still knows what it knew last week and one that has never reached them simply shows no countries. Nothing is asked of a third party, because doing this by lookup would mean handing somebody else the address of every visitor to the installation.
On top of that country, LAMBDA_LOG_GEO_PLACES adds a town and the name of
the network an address is on - "Aveiro, PT · MEO" rather than "PT" - from the
free DB-IP databases. They are about 130 MB on the data volume, refetched
monthly, and cost nothing in memory: the format is a search trie and the files
are memory mapped, so what they use is page cache the kernel can drop rather
than heap that has to be paid for. Lookups are remembered by address, because
a scanner is one address and five hundred requests: measured, a fresh address
costs about thirty-five microseconds and six hundred bytes, one already seen
costs seven hundredths of a microsecond and nothing. So the price is paid per
caller rather than per request - a thousand requests from fifty callers is two
microseconds each, and from a thousand callers is thirty-five.
Read it as the registration of a range and not the location of a person. Two
results from this machine make the point: its IPv4 address is registered in
Austria and its IPv6 address in Germany, and 1.1.1.1 - a resolver that
answers from everywhere at once - reads as Australia, because that is where
the block is registered. It tells a Portuguese visitor from a Singaporean
scanner. It does not put anybody on a map, and LAMBDA_LOG_GEO=false stops it
downloading anything at all.
The town is a guess rather than a record - measured and inferred by a third party, right about most consumer connections and wrong about most infrastructure - so it is shown as one and never as a position. Country and town data by DB-IP under CC BY 4.0, which the panel credits; the registry data is published by the RIRs themselves.
Three bounds keep it honest. The ring holds LAMBDA_LOG_HISTORY lines, up to
a million, with a cap on the length of each; LAMBDA_LOG_MEMORY_MB caps what
their text may cost, and whichever runs out first decides, so a lambda in a
print loop costs a fixed amount of memory rather than a growing one and long
lines simply mean fewer of them; and one request may only contribute
LAMBDA_LOG_MAX_LINES_PER_REQUEST before the rest is counted and dropped, so
it evicts its own output rather than everybody else's. A million ordinary
request lines measured at about 220 MB, which is the number to budget against.
Nothing here survives a restart. The container's stdout still has the whole run and is
what to read when the question is about something that happened before the
process started - reading the log through the panel is a convenience, not the
record.
The panel also says how the run before this one ended, where it did not end cleanly. The server cannot answer "did it just crash" about itself - whatever it would have said went down with it - so each run leaves a note on the data volume, refreshed on the telemetry tick, and the next one reads it. A note with no stop stamped on it is a process that went away between one heartbeat and the next, and the memory, socket count and request count it was carrying at that beat are usually most of the answer. Being asked to stop is stamped as the signal arrives rather than once the stopping is done, so a run that was told to go and died partway can be told from one that nothing asked at all. An unhandled exception is written into the note before the process goes down, because in a container that restarts immediately, stderr is easy to lose.
LAMBDA_LOG_LAMBDA_OUTPUT=false turns off the gathering of what lambdas
print, for an installation that would rather not hold a stranger's output in
memory at all. Their console still reaches stdout; it is simply not collected.
The route is behind the token without the exception the figures get: aggregates
name nobody, and this is whatever somebody's code decided to print.
The listing links the editor of each lambda, which is the whole of the editor key and therefore the whole of the credential. That is a deliberate trade: an administrator who has to decide whether something is abusive needs to read it, and reading it in the editor is also how it gets emptied or corrected rather than only deleted. It is one more reason the token belongs to a person and not in a browser somebody else uses.
SQLite through EF Core, migrated on startup by Evolve from the SQL
files in Data/Migrations. To change the schema, add the next
V<n>__<name>.sql next to them - they are embedded resources, and the
existing ones are never edited.