Running as a Netty-based server

To expose an endpoint using a Netty-based server, first add the following dependency:

// if you are using Future or just exploring:
"com.softwaremill.sttp.tapir" %% "tapir-netty-server" % "1.10.7"

// if you want to use Java 21 Loom virtual threads in direct style:
"com.softwaremill.sttp.tapir" %% "tapir-netty-server-sync" % "1.10.7"

// if you are using cats-effect:
"com.softwaremill.sttp.tapir" %% "tapir-netty-server-cats" % "1.10.7"

// if you are using zio:
"com.softwaremill.sttp.tapir" %% "tapir-netty-server-zio" % "1.10.7"

Then, use:

  • NettyFutureServer().addEndpoints to expose Future-based server endpoints.

  • NettySyncServer().addEndpoints to expose Loom-based server endpoints.

  • NettyCatsServer().addEndpoints to expose F-based server endpoints, where F is any cats-effect supported effect. Streaming request and response bodies is supported with fs2.

  • NettyZioServer().addEndpoints to expose ZIO-based server endpoints, where R represents ZIO requirements supported effect. Streaming is supported with ZIO Streams.

These methods require a single, or a list of ServerEndpoints, which can be created by adding server logic to an endpoint.

For example:

import sttp.tapir._
import sttp.tapir.server.netty.{NettyFutureServer, NettyFutureServerBinding}
import scala.concurrent.Future

val helloWorld = endpoint
  .serverLogic(name => Future.successful[Either[Unit, String]](Right(s"Hello, $name!")))

val binding: Future[NettyFutureServerBinding] =

The tapir-netty-server-sync server uses Id[T] as its wrapper effect for compatibility, while Id[A] means in fact just A, representing direct style. It is available only for Scala 3. See examples/HelloWorldNettySyncServer.scala for a full example. To learn more about handling concurrency with Ox, see the documentation.


The interpreter can be configured by providing an NettyFutureServerOptions value, see server options for details.

Some options can be configured directly using a NettyFutureServer instance, such as the host and port. Others can be passed using the NettyFutureServer(options) methods. Options may also be overridden when adding endpoints. For example:

import sttp.tapir.server.netty.{NettyConfig, NettyFutureServer, NettyFutureServerOptions}

// customising the port

// customising the interceptors

// customise Netty config

Web sockets


The Cats Effects interpreter supports web sockets, with pipes of type fs2.Pipe[F, REQ, RESP]. See web sockets for more details.

To create a web socket endpoint, use Tapir’s out(webSocketBody) output type:

import cats.effect.kernel.Resource
import cats.effect.{IO, ResourceApp}
import cats.syntax.all._
import fs2.Pipe
import sttp.capabilities.fs2.Fs2Streams
import sttp.tapir._
import sttp.tapir.server.netty.cats.NettyCatsServer

import scala.concurrent.duration._

object WebSocketsNettyCatsServer extends ResourceApp.Forever {

  // Web socket endpoint
  val wsEndpoint =
        webSocketBody[String, CodecFormat.TextPlain, String, CodecFormat.TextPlain](Fs2Streams[IO])
          .concatenateFragmentedFrames(false) // All these options are supported by tapir-netty
          .autoPing(Some((10.seconds, WebSocketFrame.Ping("ping-content".getBytes))))

  // Your processor transforming a stream of requests into a stream of responses
  val pipe: Pipe[IO, String, String] = requestStream => requestStream.evalMap(str => IO.pure(str.toUpperCase))
  // Alternatively, requests can be ignored and the backend can be turned into a stream emitting frames to the client:
  // val pipe: Pipe[IO, String, String] = requestStream => someDataEmittingStream.concurrently(

  val wsServerEndpoint = wsEndpoint.serverLogicSuccess(_ => IO.pure(pipe))

  // A regular /GET endpoint
  val helloWorldEndpoint: PublicEndpoint[String, Unit, String, Any] ="hello").in(query[String]("name")).out(stringBody)

  val helloWorldServerEndpoint = helloWorldEndpoint
    .serverLogicSuccess(name => IO.pure(s"Hello, $name!"))

  override def run(args: List[String]) = NettyCatsServer
    .flatMap { server =>
            .addEndpoints(List(wsServerEndpoint, helloWorldServerEndpoint))


In the Loom-based backend, Tapir uses Ox to manage concurrency, and your transformation pipeline should be represented as Ox ?=> Source[A] => Source[B]. Any forks started within this function will be run under a safely isolated internal scope. See examples/websocket/WebSocketNettySyncServer.scala for a full example.

Graceful shutdown

A Netty server can be gracefully closed using the function NettyFutureServerBinding.stop() (and analogous functions available in Cats and ZIO bindings). This function ensures that the server will wait at most 10 seconds for in-flight requests to complete, while rejecting all new requests with 503 during this period. Afterwards, it closes all server resources. You can customize this behavior in NettyConfig:

import sttp.tapir.server.netty.NettyConfig
import scala.concurrent.duration._

// adjust the waiting time to your needs
val config = NettyConfig.default.withGracefulShutdownTimeout(5.seconds)
// or if you don't want the server to wait for in-flight requests
val config2 = NettyConfig.default.noGracefulShutdown

Domain socket support

There is possibility to use Domain socket instead of TCP for handling traffic.

import sttp.tapir.server.netty.{NettyFutureServer, NettyFutureDomainSocketBinding}
import sttp.tapir.{endpoint, query, stringBody}

import java.nio.file.Paths
import scala.concurrent.Future


val serverBinding: Future[NettyFutureDomainSocketBinding] =
  NettyFutureServer().addEndpoint("hello").in(query[String]("name")).out(stringBody).serverLogic(name =>
      Future.successful[Either[Unit, String]](Right(s"Hello, $name!")))
  .startUsingDomainSocket(Paths.get(System.getProperty(""), "hello"))


By default, logging of handled requests and exceptions is enabled, and uses an slf4j logger.