Inngest Python SDK

repository·main·Indexed 42 days ago

https://github.com/inngest/inngest-py

A Python SDK for building durable, event-driven background jobs and workflows without managing queuing infrastructure. It supports Python 3.10+ and integrates with frameworks including FastAPI (>=0.110.0), Django (>=5.0), Flask (>=3.0.0), Tornado (>=6.4), and DigitalOcean Functions. Features include durable steps via step.run, automatic retries, state management, and an EncryptionMiddleware for securing event data and function inputs/outputs.

Tokens
13.8K
Snippets
33
Records
68
Agent score
53%

What's inside Inngest

  1. Overview of Inngest Python SDK

    main

    The Inngest Python SDK allows you to add durable functions, background jobs, and scheduled jobs to your Python applications without managing separate queuing infrastructure like Celery. It enables writing background jobs as step functions that are resilient to failures.

    Supported Frameworks:

    • DigitalOcean Functions
    • Django (>=5.0)
    • FastAPI (>=0.110.0)
    • Flask (>=3.0.0)
    • Tornado (>=6.4)

    Requirements:

    • Python 3.10 or higher.
  2. How POST execution requests are processed

    main

    When the Inngest server triggers a function, it sends a POST request to the SDK (e.g., POST /api/inngest?fnId=...&stepId=...).

    The Execution Process:

    1. Parameter Extraction: The SDK extracts fnId (the function identifier) and stepId (the specific step to target, if applicable) from the query parameters.
    2. Deserialization: The SDK deserializes the ServerRequest from the request body. This object contains the triggering event, event batch, memoized step outputs, and execution context (such as run_id and attempt).
    3. Payload Fetching: If the payload is too large for a single request, the Executor sets use_api=true. In this case, the SDK fetches the event batch and step memos from the Inngest API instead of reading them from the request body.
    4. Function Invocation: The SDK prepares the arguments and calls the user-defined function.
  3. Understand how the SDK syncs with the Inngest server

    main

    Before the Inngest server can execute functions, it must know their configurations (triggers, concurrency limits, retry policies, etc.). This process is called Syncing.

    Syncing is triggered when the Inngest server sends a PUT request to the SDK's serve endpoint. The mechanism used is determined by the x-inngest-sync-kind header. The SDK supports two modes:

    1. In-Band Sync: The SDK responds directly to the PUT request with function configs and app metadata. This is the preferred mechanism and requires request signature verification.
    2. Out-of-Band Sync: The SDK makes a separate HTTP POST to the Inngest API's /fn/register endpoint using the signing key hash as a Bearer token, then forwards that response back to the original PUT caller.
  4. Understand the Connect mechanism

    main

    The Connect mechanism uses a WebSocket-based handshake instead of standard HTTP PUT requests.

    During the handshake, the SDK sends a WORKER_CONNECT protobuf message containing the JSON-encoded function configurations. When using Connect, the runtime URLs use the wss://connect scheme instead of http, and the runtime type is identified as ws rather than http.

  5. Understand the Connect threading model

    main

    The Connect implementation uses a dual-thread model to ensure that communication with the Inngest server is never blocked by user code:

    • Worker thread: Runs its own event loop. It manages the WebSocket connection, handshake, heartbeats, lease extensions, and message dispatch. Execution requests are forwarded to the main thread using asyncio.run_coroutine_threadsafe().
    • Main thread: Runs the actual user function code via CommHandler and handles OS signals (like SIGTERM) for graceful shutdown.
  6. How flushing handles unacknowledged replies

    main

    If a WebSocket connection drops after a worker completes an execution but before the reply is acknowledged, the SDK uses Flushing to ensure the result is delivered.

    Unacknowledged replies are sent via HTTP to the /v0/connect/flush endpoint. A periodic poller automatically checks for these unacked replies and flushes them. To prevent Out-Of-Memory (OOM) risks, unacked replies are held in a buffer rather than being kept in memory indefinitely.

  7. Understand the Inngest Connect lifecycle

    main

    The Connect implementation follows a specific lifecycle to maintain a live connection between the SDK and the Inngest server:

    1. Bootstrap: The SDK calls /v0/connect/start via REST to obtain a WebSocket URL, connection ID, and auth tokens.
    2. WebSocket open: The SDK establishes a connection to the gateway using protobuf messages.
    3. Handshake: The SDK sends a WORKER_CONNECT message containing function configurations and app metadata, then waits for a ready signal from the server.
    4. Active: The connection is live; the SDK receives execution requests, runs functions, and sends results.
    5. Reconnect: If the connection fails, the SDK re-runs the bootstrap and handshake process. It can skip the previous gateway to avoid bad nodes.
    6. Shutdown: Upon receiving SIGTERM or SIGINT, the SDK sends a pause message, waits for in-flight requests to drain, and then closes.
  8. Understand Signing Keys and Rotation

    main

    The Inngest Python SDK uses signing keys to authenticate requests between the Inngest server and your application. It supports two types of keys to facilitate zero-downtime rotation:

    1. Primary signing key: The main key used for signing and verification.
    2. Fallback signing key: An optional second key. To rotate keys without downtime, set the new key as the primary and the old key as the fallback. The SDK will accept both until the fallback is removed.

    Note on Environments:

    • Production: A signing key is required.
    • Dev Mode: Signature verification is automatically skipped.
  9. How function on-failure handlers are registered

    main
    When using on-failure handlers, they are registered as separate functions. The SDK assigns them an ID following the pattern {parent_id}-failure. These handlers are triggered by the internal inngest/function.failed event, filtered by the parent function's ID.
  10. How parallel execution works with Group.parallel()

    main

    Parallel execution allows multiple steps to run concurrently on the Inngest Executor. This is achieved through a discovery phase using Group.parallel():

    1. The SDK sets an in_parallel context variable to True.
    2. Each step called within the parallel group is checked for a memo.
    3. If a step is new (no memo), it is marked as PLANNED and a ResponseInterrupt is raised.
    4. The SDK collects all planned steps and raises a single combined ResponseInterrupt.
    5. The Inngest Executor receives these planned steps and executes them concurrently.
    6. Step Targeting: To run these steps, the Executor uses a stepId query parameter to tell the SDK which specific step to execute during subsequent replays, while other steps in the group raise a SkipInterrupt to be ignored.
  11. How the Inngest Python execution model works

    main

    Inngest functions use a deterministic replay model. An Inngest function is called multiple times per run—once for each step (or group of parallel steps) that needs to execute.

    Each time the function is called, it replays from the top. The SDK uses StepMemos (memoized results) to skip steps that have already completed. When the SDK encounters a step with no memo, it either executes the step or 'plans' it (for parallel or deferred execution) and then interrupts the function to return control to the Inngest server.

    **Key behaviors:

    • Deterministic code is ideal: The SDK works best when steps appear in the same order on every replay.
    • Non-determinism handling: If a new step appears without a memo, it runs. If a memoized step is no longer encountered, its memo is ignored.
    • Error condition: An error occurs if step targeting is active and the targeted step is not found during replay.
    @client.create_function(
        fn_id="my-fn",
        trigger=inngest.TriggerEvent(event="app/signup"),
    )
    async def my_fn(ctx: inngest.Context) -> str:
        user = await ctx.step.run("create-user", create_user)       # Step A
        await ctx.step.run("send-email", lambda: send_email(user))  # Step B
        return "done"
  12. How the Inngest request lifecycle works

    main

    The Inngest server (the "Executor") drives function execution by sending HTTP requests or WebSocket messages to the SDK. The SDK is reactive and only responds to these requests.

    The Replay Model A single function run typically involves multiple requests. Each request replays the function from the top. To ensure efficiency and durability, the SDK uses memoized step results from prior requests to skip work that has already been completed. This replay-based model is the core of Inngest's durable execution.

    Execution Flow

    1. Entry Point: A request enters through a framework integration (e.g., flask.py, fast_api.py, django.py). These adapters normalize the framework's request into a CommRequest.
    2. Dispatch: The request is passed to the CommHandler, which acts as the central dispatcher.
    3. Execution: For POST requests, the SDK deserializes a ServerRequest containing the event, memoized step outputs, and execution context (run ID, attempt number).
    4. Response: The SDK converts the function's result into a CommResponse and sends it back to the Executor.