Embassy: a small async HTTP server for Swift that runs inside your test
Super lightweight async HTTP server library in pure Swift runs in iOS / MacOS / Linux
At a glance
- What is it?
- Embassy is a pure Swift asynchronous HTTP server of about 1.5 thousand lines with no third party dependencies, built around a WSGI style gateway called SWSGI. Its release tags stopped in 2023 while the repository kept receiving commits into 2026.
- Who is it for?
- Embassy suits a team that needs an HTTP endpoint inside a test harness, a simulator run or a local tool, where pulling in a full server stack would be absurd, and where the SWSGI shape is familiar enough to read. It does not suit a service that has to survive the public internet, because the release tags are at v4.1.6 from 2023 while the last push was on 2026-10-02 and the README does not describe a security or upgrade policy.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 20 days ago.
- What is it written in?
- Mainly Swift, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A server sized for a test process
Embassy is described as a super lightweight async HTTP server in pure Swift, and the feature list gives the numbers: super lightweight, only 1.5 K of lines, and zero third-party dependency. The supported platforms are iOS, tvOS, macOS and Linux, and the project is compatible with Carthage, Swift Package Manager and CocoaPods.
The intended use is spelled out in the article the README links as a must-read, Embedded web server for iOS UI testing. That is the design brief in one line. If your UI test needs to reach a local endpoint to seed data, verify a state transition or fetch a fixture, spinning up a full application server inside a simulator process is heavy, and pulling a networking framework into an app target is worse. A 1.5 thousand line server with no dependencies is the size of thing you can drop into a test target.
The other clue is the same file tree. The repository holds an Xcode project and workspace, a Package.swift, Sources/, Tests/, a SwiftLint configuration, a Hound configuration, a pre-commit configuration, a Code Climate configuration, a CLAUDE.md and a run-docker.sh script. Automatic testing is listed as a feature, and the tree shows both a Tests/ directory and a Linux friendly shell script, which points at a library that is developed and exercised on a developer Mac and on Linux at the same time.
The README also points to Ambassador, a lightweight web framework built on Embassy, which is worth knowing about before you write your own routing.
The event loop is where the concurrency lives
Embassy is not thread based. It is an async event loop HTTP server, and the README says that this makes long-polling, delay and bandwidth throttling all possible.
The loop is created explicitly and handed to the server.
let loop = try! SelectorEventLoop(selector: try! KqueueSelector())An application gets the loop from the environ dictionary under the key embassy.event_loop, casts it to EventLoop, and schedules work on it. Three scheduling methods are documented: call(callback:) runs a callback as soon as possible in the event loop, call(withDelay:callback:) schedules it a number of seconds later, and call(atTime:callback:) schedules it for a given Date, behaving like the plain call when the time has already passed.
let loop = environ["embassy.event_loop"] as! EventLoop
loop.call(withDelay: 1) {
sendBody(Data("hello ".utf8))
}The threading rule is stated bluntly and is the part to internalise. Functions passed into SWSGI should only be called within the same thread that runs the EventLoop, they are not thread safe, and you should not use GCD for delaying any call. The EventLoop methods themselves are the thread safe ones. So the model is a single threaded loop where your handler schedules its own future work, and anything that tries to hop threads mid request is a bug rather than a race you can ignore.
SWSGI is WSGI with the keys renamed
The application interface is a typealias, and it is a deliberate hat tip to Python's WSGI, the point being that a web application should talk to HTTP clients without knowing which server implementation is underneath.
public typealias SWSGI = (
[String: Any],
@escaping ((String, [(String, String)]) -> Void),
@escaping ((Data) -> Void)
) -> VoidThe environ dictionary follows the WSGI standard except that wsgi.* keys become swsgi.*. The documented example carries the usual entries, SERVER_NAME, SERVER_PROTOCOL, SERVER_PORT, REQUEST_METHOD, SCRIPT_NAME, PATH_INFO, the HTTP_* header keys, and the swsgi.* set including swsgi.version, swsgi.input, swsgi.error, swsgi.multiprocess, swsgi.multithread, swsgi.url_scheme and swsgi.run_once.
Three Embassy specific keys are added on top: embassy.connection, an HTTPConnection object for the request, embassy.event_loop, the EventLoop object, and embassy.version, the version of Embassy as a string.
Reading the request body goes through swsgi.input, which is cast to SWSGIInput and called with a data handler. An empty Data is passed to the handler when EOF is reached. The detail worth remembering is that the request body will not be read if swsgi.input is not set or is set to nil, and the README suggests using that as bandwidth control, setting it to nil when you do not want to receive any data from the client. That single switch is how you stop a client from streaming a large body into a process that never wanted it.
startResponse once, then sendBody, then an empty body
The response side has strict rules, and they are simple enough to state in full.
startResponse sends the response header, taking a status code with its message as the first argument and headers as a list of key and value tuples.
startResponse("200 OK", [("Set-Cookie", "foo=bar")])It can only be called once per request, and an extra call is simply ignored. sendBody sends body data to the client, and it requires that startResponse has already been called: if you call sendBody first, all calls to sendBody will be ignored.
End of response is signalled by sending an empty Data, as the example does with a comment for send EOF. In the async example, the final scheduled callback sends the last chunk and then the empty Data, which is how the connection knows the response is complete.
The whole exchange fits in a single closure, which is why the library can be this small. There is no request object, no response builder and no middleware chain. A handler receives a dictionary, sends a status line, pushes bytes, and finishes. The cost of that simplicity is that anything a richer framework would give you, such as routing, you write yourself, which is precisely the gap that Ambassador, built on Embassy, fills.
Binding to all interfaces is an explicit decision
By default the server binds only to the localhost interface, which is written as ::1, and the README gives the reason as security.
The default example constructs a DefaultHTTPServer with an event loop and a port, and the handler is a closure that takes the environ dictionary, a startResponse function and a sendBody function.
let server = DefaultHTTPServer(eventLoop: loop, port: 8080) {
(
environ: [String: Any],
startResponse: ((String, [(String, String)]) -> Void),
sendBody: ((Data) -> Void)
) in
// Start HTTP response
startResponse("200 OK", [])The server is started with try! server.start(), and the event loop is then run with loop.runForever(). With that in place you can visit http://[::1]:8080/foo-bar in a browser and see the path echoed back, because the handler reads PATH_INFO from environ and writes it into the body.
If you need the server reachable from another machine, you change the bind interface to all addresses.
let server = DefaultHTTPServer(eventLoop: loop, interface: "::", port: 8080)Note that the IPv6 ready, dual stack support is listed as a feature, so the all addresses interface is an IPv6 wildcard that also covers IPv4. The default being localhost is the safer choice and the README frames it that way, which is the right default for a library that often ends up inside a test process on a shared machine.
Two ways to depend on it
The dependency story is simple and covers the three ecosystems a Swift project might use. There is a Carthage compatible badge, a Swift Package Manager compatible badge and a CocoaPods badge, and the repository carries a Package.swift alongside an Embassy.xcodeproj and an Embassy.xcworkspace.
That last detail is the one to check. A package with both a Package.swift and an Xcode workspace is a library that predates the Swift Package Manager era and has kept both paths alive, which is common for a library with the kind of user base this one has. A SwiftPM consumer adds the package and writes an SWSGI closure; a Carthage or CocoaPods consumer integrates the sources into their workspace and does the same thing with the module.
The repository also shows the tooling the maintainers use rather than the ones they impose on you. There is a SwiftLint configuration, a Hound configuration for style review on pull requests, a pre-commit configuration, a Code Climate configuration, and a run-docker.sh script, which suggests the test suite runs somewhere other than only a developer laptop.
The related framework is worth a sentence. The README points to Ambassador as a lightweight web framework based on Embassy, which is the piece to reach for if you want routing and request handling helpers but still want the same event loop underneath rather than a heavier stack.
Tags from 2023, commits into 2026
The release list shows v4.1.6 on 2023-01-30, v4.1.5 on 2023-01-27 and v4.1.4 on 2022-10-12, and the last push to the repository was on 2026-10-02. The repository is not archived, so the tree is being worked on, but the published tags stopped more than three years ago.
One of the older tag titles is informative about the kind of maintenance that does happen. v4.1.4 is titled as a fix for an Xcode 14 withMemoryRebound crash, which tells you that the project has needed to track Apple toolchain changes and that a release was cut for a crash rather than a feature.
The Swift version story is also worth noting. The features list says Swift 4 and 5, the badge shows Swift 5.0, and the package lives in the Swift Package Manager and Carthage and CocoaPods world of that era. If you are on a current toolchain, the question is whether the vendored source still compiles, and the repository state rather than the tag list is what answers that.
The licence is MIT, with the terms in the LICENSE file at the repository root, so vendoring the source into a test target carries no unusual obligation. The practical upgrade cost of this project is therefore not licensing but review: you are taking a small, understandable body of code that a maintainer has been committing to for years without cutting releases.
Editorial conclusion
Embassy suits a team that needs an HTTP endpoint inside a test harness, a simulator run or a local tool, where pulling in a full server stack would be absurd, and where the SWSGI shape is familiar enough to read. It does not suit a service that has to survive the public internet, because the release tags are at v4.1.6 from 2023 while the last push was on 2026-10-02 and the README does not describe a security or upgrade policy. Verify first which commit you vendor, and check the Swift version your toolchain needs, since the feature list names Swift 4 and 5 while the badge shows Swift 5.0.
Frequently asked questions
How do I start an Embassy HTTP server in Swift?
Create a SelectorEventLoop with a KqueueSelector, pass it to DefaultHTTPServer with a port and your SWSGI closure, then call server.start() and loop.runForever() to run the event loop. The example binds to localhost on port 8080 by default.
How do I read the request body in Embassy?
Cast environ["swsgi.input"] to SWSGIInput and call it with a data handler. An empty Data is passed to the handler at EOF, and the body is not read at all if swsgi.input is nil, which the README suggests using for bandwidth control.
Does Embassy listen on all network interfaces by default?
No. By default it binds only to the localhost interface, ::1, for security reasons. To expose it on the network you construct the server with interface: "::".
Can I use GCD to delay a response in Embassy?
The README says functions passed into SWSGI should only be called within the same thread running the EventLoop, they are not thread safe, and GCD should not be used for delaying any call. Use the EventLoop methods call(callback:), call(withDelay:callback:) and call(atTime:callback:) instead.
What licence does Embassy use?
The project is MIT licensed, with the licence file referenced from the repository root as LICENSE.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/envoy-embassy)Was this useful?