Background Webhook Processing
Telegraph handles incoming webhook updates synchronously by default. The webhook controller resolves the bot and configured WebhookHandler, calls the handler, and returns HTTP 204 only after the handler finishes.
Telegraph::dispatch() does not move this inbound handler work to a queue. It queues only the outbound request from your application to the Telegram Bot API.
For outbound queue selection, worker pools, Horizon, Telegram limits, and current 429 behavior, see Queued Messages.
Keep webhook handlers short
Webhook handlers should validate the update, extract the identifiers required by the application, dispatch heavy work, and finish quickly. Slow API calls, report generation, large database operations, and similar work belong in application-owned Laravel jobs.
For callback queries using an asynchronous queue connection, validate and authorize the input, dispatch the application job, and then acknowledge the callback. Keep validation and queue dispatch short so the acknowledgement remains prompt:
namespace App\Http\Webhooks; use App\Jobs\GenerateReport; use App\Models\Report; use DefStudio\Telegraph\Handlers\WebhookHandler; class CustomWebhookHandler extends WebhookHandler { public function generateReport(): void { $updateId = (int) $this->request->input('update_id'); $reportId = filter_var( $this->data->get('report-id'), FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]], ); $report = $reportId === false ? null : Report::query() ->whereKey($reportId) ->where('telegram_chat_id', (string) $this->chat->chat_id) ->first(); if ($report === null) { $this->reply('Report not available', true); return; } GenerateReport::dispatch( reportId: (int) $report->getKey(), telegraphBotId: (string) $this->bot->getKey(), telegramChatId: (string) $this->chat->chat_id, updateId: $updateId, )->onQueue('telegram-inbound'); $this->reply('Report queued'); } }
Replace the report ownership scope with the host application's authorization policy. Callback data is untrusted request input and must not grant access to a record by itself.
The job receives stable scalar identifiers and loads the records it needs when it runs. The Telegram chat ID remains available even when unknown chats are allowed without storing a TelegraphChat model. Do not serialize the HTTP Request, a WebhookHandler, service clients, bot tokens, webhook secrets, or an entire raw update into the job payload.
Separate inbound and outbound work
An application job and an outbound Telegraph request are two different queue jobs:
namespace App\Jobs; use App\Models\Report; use DefStudio\Telegraph\Facades\Telegraph; use DefStudio\Telegraph\Models\TelegraphBot; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\DB; class GenerateReport implements ShouldQueue { use Dispatchable; use InteractsWithQueue; use Queueable; use SerializesModels; public function __construct( public int $reportId, public string $telegraphBotId, public string $telegramChatId, public int $updateId, ) { } public function handle(): void { Report::query() ->whereKey($this->reportId) ->where('telegram_chat_id', $this->telegramChatId) ->firstOrFail(); $timestamp = now(); $claimed = DB::table('processed_telegram_updates')->insertOrIgnore([ 'telegraph_bot_id' => $this->telegraphBotId, 'update_id' => $this->updateId, 'created_at' => $timestamp, 'updated_at' => $timestamp, ]); if ($claimed === 0) { return; } /** @var class-string<TelegraphBot> $botModel */ $botModel = config('telegraph.models.bot'); /** @var TelegraphBot $bot */ $bot = $botModel::query()->findOrFail($this->telegraphBotId); // Perform the application work and build the result... Telegraph::bot($bot) ->chat($this->telegramChatId) ->message('Your report is ready') ->dispatch('telegram-interactive'); } }
In this example:
telegram-inboundcontains application processing triggered by webhook updates.telegram-interactivecontains outbound Telegram API requests.- Each queue can have its own worker pool, timeout, retry policy, and capacity.
Keep queue names fixed and operationally manageable. Do not create a queue for every chat or update.
Idempotency and ordering
Queue jobs may be attempted more than once, and webhook updates can be redelivered. Treat the bot identity together with Telegram's update_id as an idempotency input.
The processed_telegram_updates table in the example is application-owned and must have a composite unique database index on telegraph_bot_id and update_id. insertOrIgnore() then provides an atomic duplicate-prevention claim before report generation and outbound dispatch.
This simple claim provides at-most-once processing: a process failure after the claim can suppress a later retry. For work that must recover after a crash, store claim status and lease expiry, make the domain operation idempotent, or use a transactional outbox. Do not delete a claim blindly after an error because an external side effect may already have completed.
Multiple workers can process updates concurrently, so queue order is not a per-chat ordering guarantee. When order matters, enforce it in the host application with durable sequence state, locks, or another domain-specific coordination mechanism.
Do not acknowledge an application action as complete merely because its job was dispatched. The job can still fail after the webhook has returned HTTP 204.
Worker failures
Configure Laravel failed-job storage and monitor queue depth, wait time, and failed-job count for the inbound and outbound queues separately. Retry policy belongs to the host application and should match whether the application operation is idempotent.
Keep runtime logging minimal. Use sanitized job or update identifiers only for WARN and ERROR events needed to investigate failures. Do not write raw webhook payloads, bot tokens, secret headers, message content, or chat and user identifiers to shared logs.
max_connections is not worker capacity
telegraph.webhook.max_connections is sent to Telegram when the webhook is registered. It controls how many simultaneous HTTPS connections Telegram may use to deliver updates to the webhook.
It does not configure Laravel queue workers, Horizon balancing, inbound job concurrency, outbound request concurrency, or Telegram API rate limiting. Size those parts independently in the host application.