Skip to main content
Version: 3.4

Running a web server

In this guide, you'll learn how to run a web server inside your Apify Actor. This is useful for monitoring Actor progress, creating custom APIs, or serving content during the Actor run.

Introduction

Each Actor run on the Apify platform is assigned a unique hard-to-guess URL (for example https://8segt5i81sokzm.runs.apify.net), which enables HTTP access to an optional web server running inside the Actor run's container.

The URL is available in the following places:

  • In Apify Console, on the Actor run details page as the Container URL field.
  • In the API as the containerUrl property of the Run object.
  • In the Actor as the Actor.configuration.web_server_url property.

The web server running inside the container must listen at the port defined by the Actor.configuration.web_server_port property. When running Actors locally, the port defaults to 4321, so the web server will be accessible at http://localhost:4321.

Example Actor

The following example shows how to start a simple web server in your Actor, which will respond to every GET request with the number of items that the Actor has processed so far:

Run on
import asyncio
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

from apify import Actor

processed_items = 0
http_server = None


class RequestHandler(BaseHTTPRequestHandler):
"""A handler that prints the number of processed items on every GET request."""

def do_GET(self) -> None:
self.log_request()
self.send_response(200)
self.end_headers()
self.wfile.write(bytes(f'Processed items: {processed_items}', encoding='utf-8'))


def run_server() -> None:
"""Start the HTTP server and keep a reference to it."""
global http_server
with ThreadingHTTPServer(
('', Actor.configuration.web_server_port), RequestHandler
) as server:
Actor.log.info(f'Server running on {Actor.configuration.web_server_port}')
http_server = server
server.serve_forever()


async def main() -> None:
global processed_items
async with Actor:
# Start the HTTP server in a separate thread.
run_server_task = asyncio.get_running_loop().run_in_executor(None, run_server)

# Simulate doing some work.
for _ in range(100):
await asyncio.sleep(1)
processed_items += 1
Actor.log.info(f'Processed items: {processed_items}')

if http_server is None:
raise RuntimeError('HTTP server not started')

# Signal the server to shut down and wait.
http_server.shutdown()
await run_server_task


if __name__ == '__main__':
asyncio.run(main())

Using FastAPI

The example relies only on Python's standard library, which keeps it dependency-free but leaves you handling requests by hand. For anything beyond a single endpoint, a web framework such as FastAPI is a better fit. It gives you routing, request parsing, and automatic JSON responses, and is served by an ASGI server like uvicorn.

Install both, for example by adding them to your requirements.txt:

fastapi
uvicorn[standard]

The following Actor serves the same processed-items counter as before, but through a FastAPI endpoint. The key difference is that uvicorn runs inside the Actor's event loop as a background task, bound to Actor.configuration.web_server_port. The platform then routes the container URL to it:

Run on
import asyncio

import uvicorn
from fastapi import FastAPI

from apify import Actor

# Counter the server reports and the Actor updates.
processed_items = 0

# FastAPI app with a single endpoint.
app = FastAPI()


@app.get('/')
async def index() -> dict[str, int]:
"""Respond to every GET request with the number of processed items."""
return {'processed_items': processed_items}


async def main() -> None:
global processed_items
async with Actor:
# Serve the app on the platform's web server port. Binding to 0.0.0.0
# makes it reachable through the container URL.
config = uvicorn.Config(
app,
host='0.0.0.0', # noqa: S104
port=Actor.configuration.web_server_port,
)
server = uvicorn.Server(config)

# Run the server in the background.
server_task = asyncio.create_task(server.serve())
Actor.log.info(f'Server running at {Actor.configuration.web_server_url}')

# Simulate work, updating the reported counter.
for _ in range(100):
await asyncio.sleep(1)
processed_items += 1
Actor.log.info(f'Processed items: {processed_items}')

# Signal the server to shut down and wait.
server.should_exit = True
await server_task


if __name__ == '__main__':
asyncio.run(main())

Note that:

  • uvicorn.Server(...).serve() is a coroutine. It runs as an asyncio task alongside the Actor's own work instead of blocking it. Setting server.should_exit = True triggers a graceful shutdown once the work is done.
  • The server binds to 0.0.0.0 (all interfaces) rather than localhost. This makes it reachable through the container URL, not only from inside the container.
  • The same pattern powers an Actor Standby service. Swap the one-off work loop for an Actor that keeps serving requests.

Actor Standby

The example runs a web server for the duration of a single Actor run. With Actor Standby, you can instead expose your Actor as an always-ready HTTP API: the platform keeps the Actor running in the background and routes incoming HTTP requests to the web server inside it, spinning up additional instances as the load grows.

From the SDK's perspective, a Standby Actor is built the same way as the web server above. You start an HTTP server listening on the port from Actor.configuration.web_server_port. The difference is operational: instead of doing its work once and exiting, a Standby Actor stays up and serves requests. This makes it a good fit for low-latency, on-demand use cases, such as serving scraped data or acting as a microservice.

To get started, use the Standby Python template. For details on enabling Standby, request routing, and readiness probes, see the Actor Standby documentation.

Conclusion

In this guide, you learned how to run a web server inside your Apify Actor. By leveraging the container URL and port provided by the platform, you can expose HTTP endpoints for monitoring, reporting, or serving content during Actor execution. If you have questions or need assistance, feel free to reach out on our GitHub or join our Discord community.

Additional resources