# Asynchronous File Generation and Download with AWS AppSync, Lambda, and EventBridge

In a previous article, I explained how to generate [**Amazon S3 pre-signed download URLs** using AWS AppSync](https://blog.graphbolt.dev/how-to-download-and-upload-files-to-amazon-s3-with-aws-appsync). But what happens when the file doesn’t exist yet?

Sometimes the file must be generated on demand. Maybe it’s a report for a user-selected date range, a data export based on filters, or something that only makes sense at that specific moment.

You might be tempted to generate that file directly within the AppSync resolver (e.g. a Lambda function), store it on S3 and then return the pre-signed URL; but this would be an anti-pattern.

* **AppSync resolvers should stay thin**  
    Resolvers are not meant to perform heavy processing. Long-running or compute-heavy tasks don’t belong there.
    
* **Hard time limits**  
    [AppSync operations have a **30-second timeout**](https://blog.graphbolt.dev/the-aws-appsync-limits-you-need-to-know). File generation, especially for large datasets, can easily exceed that.
    
* **Reliability & retries**  
    File generation can fail. You may want retries, dead-letter queues, monitoring, and proper error handling — all of which are far easier to implement asynchronously.
    

In this article, I’ll show you how to properly decouple file generation from the AppSync request using Asynchronous Lambda invocation, Amazon EventBridge, and AppSync Subscriptions.

## Architecture Overview

AWS AppSync supports long-running operations by allowing [asynchronous Lambda function invocations](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-lambda-js.html#async-invocation-type-js) as a data source (resolver). When the Lambda function is invoked asynchronously, the AppSync resolver returns immediately, while the Lambda function is allowed to run for up to 15 minutes.

We can leverage this capability to trigger our file-generation process without blocking the user's request.

Once the Lambda function finishes generating the file, it can store it in an Amazon S3 bucket, generate a pre-signed download URL, and send it back to the user through a Subscription using the [AppSync–EventBridge integration](https://docs.aws.amazon.com/eventbridge/latest/userguide/target-appsync.html).

![PlantUML diagram](https://cdn-0.plantuml.com/plantuml/png/ZL71ZjGm3BtdAxpbCaXCk-nogX3QMGJIIY16L6ZFQUhf1DAaYfqPyVSuxGogK44SgjX-p_PxxWEnZfm6fsuwF5YFi7jkzfB3aNcakggYFfUzvMDg4s4qmJDwBaqOAdqcfrlAxzOAGDj3YDlHQPy7LgUxs_AswYonKkh6UkDIXocwwuPAtlTX688lHqb_KvPuOuVHsOF5RkQipnc5OpJRu9yyOn-dC9URdzxCZMjzidnCZDKdFRIT_ZkluH1rZzKa1YzPW_5a3JucymFvMUxpfVsEXcbbpBzbVZYXvyTeknul7hMH5P2l3PgW-2P1eIvkDdgqhw2uPB3RFnMI5Y_X1KVjQTHHfgdPqQP7Zx85Frg7mieFx6CmI1JYMjz2omQdE97G5kMrhl-wowQTAjBUtCEChirIQlBzTliEnAcI3oHS6gdXTGaxokw_8WLtmgYMUERFksguAo7BGZW8y0Uh2sSlrAlD6kvBsAVMMJh2P5XT8PZ5rF2cXC-9fz-c7j1tQN2_0000 align="left")

[Source](https://www.plantuml.com/plantuml/uml/ZL7VYzGm47xFNp5NNrPms_FEfo9oUw9meBXOv9xJTBORJPEIcUpYV--atPKYLIoK-V5zy-ERF13ho6bmwgR3W_K8k-TcBp4ScKUggohgSzbRFgOs4KOJFAFdqeIXqcTokgLijrO0sXv2t8vE-zomEjtTbhTLPucLKZVM6vSwJD5T9rHYtuTX2BuS9VnDMUAD7KPd3nQxcRFUPnIEqMo3V_ACVPpSd6v-UZCNhjN9y34nLf_qq7Rwxxo6GzGzLjCoU8aQZ2Thy36P7yZFSfylxNSqJ2jj_htcXnDwVeosw_6YKHiP0VdQe0cAR-8Kwg9ceqVx2efB1BlzKo5XyH9USDIUHXsbcveLRNhqA5FmetKeh6nW7uD1eX3NwXQxR73o4mbjaLwh-g-xRDef8-NDFSpeMY-b9jrlj-z0b2hvGToba1fUbx2ZhFuhKd0dZ6g9P_uyguQx4l9Q20S1_c1ruUo5UfLPmtq9-zGwIqUO1Aih1FCoDRmfuNFYwTcKV0_ekpIuNm00)

## High-level flow

We use three [GraphQL operations](https://github.com/bboure/appsync-async-file-download/blob/main/schema.gql):

`generateReport`  
This is the Mutation the user calls to express the intent to generate a file. It receives, for example, a start date and an end date and returns a `requestId` that uniquely identifies the process.

`onReportGenerated`  
This is a Subscription that the client uses to subscribe to updates. It takes a `requestId` as input — the same `requestId` returned by the `generateReport` Mutation.  
It is essentially a way of saying:

> “Notify me once this specific report has been generated.”

`reportGenerated`  
This is another Mutation that the backend invokes to trigger the above Subscription.

### Workflow

1. The client invokes the `generateReport` Mutation.
    
2. The AppSync JS resolver generates a random `requestId`, invokes the `GenerateReport` Lambda function asynchronously, and passes it the `requestId` along with the user inputs.
    
3. AppSync immediately returns the `requestId` to the client.
    
4. The client receives the `requestId` and uses it to subscribe via `onReportGenerated`.
    
5. Once the Lambda function finishes generating the file, it stores it in an Amazon S3 bucket.
    
6. The Lambda function publishes a `reportGenerated` event to the event bus, including the `requestId` and the pre-signed download URL.
    
7. The AppSync `reportGenerated` Mutation target is triggered through the EventBridge integration.
    
8. AppSync pushes the event back to the client via the active Subscription.
    
9. The client receives the download URL and initiates the file download in the browser.
    

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You can find the full implementation on <a target="_self" rel="noopener noreferrer nofollow" href="https://github.com/bboure/appsync-async-file-download/blob/main/lib/async-file-download-stack.ts" style="pointer-events: none">Github</a></div>
</div>

### Error Handling and Other Considerations

When using asynchronous Lambda invocations, error handling behaves differently from synchronous calls.

By default, if an asynchronous Lambda execution fails, AWS automatically [**retries it twice after the initial failure**](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-error-handling.html) (three attempts in total). If all retry attempts fail and no additional configuration is in place, the event is discarded.

If you need more control over failure handling, retries, and observability, there are several options to consider:

**Lambda Destinations**

You can configure [Lambda function destinations](https://aws.amazon.com/blogs/compute/introducing-aws-lambda-destinations/) for asynchronous invocations. This allows you to route failed events to another target — such as:

* An SQS queue (acting as a DLQ)
    
* An SNS topic
    
* EventBridge
    
* Another Lambda function
    

This gives you visibility into failures and the ability to reprocess or inspect failed events.

**Introduce an SQS Queue**

Instead of invoking the Lambda function asynchronously and directly from AppSync, you can [place the event onto an **Amazon SQS queue**](https://blog.iamjkahn.com/posts/2019/12/invoking-even-more-aws-services-directly-from-aws-appsync).

The Lambda function then consumes messages from the queue.

This approach provides:

* A configurable retry policy
    
* Built-in dead-letter queue support with redrive capability
    
* Better control over failure handling
    
* Buffering and backpressure management
    

## Conclusion

In this article, we explored how to handle on-demand file generation in a clean, event-driven way using asynchronous Lambda invocations, EventBridge, and AppSync Subscriptions. Instead of blocking a GraphQL request, we decouple processing from the client interaction, allowing long-running tasks to execute reliably while still delivering real-time feedback when the file is ready.
